Compare commits
79 Commits
Author | SHA1 | Date | |
---|---|---|---|
c74fc25a2f | |||
a2773e2705 | |||
f1e37c9ff5 | |||
56bf8b0293 | |||
e2f616df73 | |||
2f94ed4305 | |||
21abc54ccd | |||
5b0f184ea6 | |||
8f5aed806c | |||
641a9cd40e | |||
c510019165 | |||
6bd734d50b | |||
b201e9d789 | |||
99af00d0d7 | |||
ffe384916d | |||
91c355ccde | |||
131329c95a | |||
bdb295b207 | |||
c3b7c9824d | |||
2e8a090f53 | |||
0337c40a05 | |||
b3b8527746 | |||
edbfc8dd62 | |||
51c51ea9ed | |||
af010ddd55 | |||
21b35d489b | |||
76022bc3e1 | |||
59a1248bf6 | |||
ec972bc5a1 | |||
4190a21ba2 | |||
835200e5e6 | |||
7565b29a37 | |||
b43467459b | |||
3823ba32b0 | |||
142b91685b | |||
7691f6f820 | |||
41d93d08ec | |||
4e7b20e593 | |||
ac50961dde | |||
a1b851fd3d | |||
ea3b0e1e86 | |||
45244d7188 | |||
a9da7f91c3 | |||
d3fcaf352d | |||
25ff6427ff | |||
d3825ce466 | |||
65a4a527eb | |||
9f4cf82142 | |||
637bf4d95b | |||
4ecd66ec34 | |||
58fb822d6f | |||
001b585bb0 | |||
71cac1fcda | |||
45883d6f21 | |||
caefc2a88f | |||
bfc6ea5939 | |||
86ab1be93c | |||
599bd71e7b | |||
9b21a9ebff | |||
dee1f090a7 | |||
28ec9d0751 | |||
4c26ef6485 | |||
fe3077cb55 | |||
3e62896e31 | |||
709ab3ca78 | |||
d207cd20b9 | |||
86b3fc8029 | |||
4c980beb90 | |||
9a4bf1c641 | |||
7d098d3595 | |||
b65b417db6 | |||
95814b7b7c | |||
b53a512d3a | |||
7a38f1513f | |||
a524395ec2 | |||
e223fb3dd4 | |||
eb90fb5909 | |||
1a11510354 | |||
8bb9c0ed0b |
54
README.md
54
README.md
|
@ -1,22 +1,14 @@
|
||||||
# gnuboard5-activitypub
|
# gnuboard5-activitypub
|
||||||
ActivityPub implementation for GNUBOARD 5
|
GNUBOARD5-ActivityPub: ActivityPub (Fediverse) implementation for GNUBOARD5
|
||||||
|
|
||||||
## References
|
* https://sir.kr/g5_plugin/10381
|
||||||
* https://www.w3.org/TR/activitypub/
|
* https://codeberg.org/fediverse/delightful-activitypub-development
|
||||||
* https://www.w3.org/TR/activitystreams-core/
|
|
||||||
* https://www.w3.org/TR/activitystreams-vocabulary/
|
|
||||||
* https://github.com/w3c/activitypub/issues/194
|
|
||||||
* https://docs.joinmastodon.org/spec/webfinger/
|
|
||||||
* https://organicdesign.nz/ActivityPub_Code
|
|
||||||
* https://socialhub.activitypub.rocks/t/posting-to-pleroma-inbox/1184
|
|
||||||
* https://github.com/broidHQ/integrations/tree/master/broid-schemas#readme
|
|
||||||
* https://github.com/autogestion/pubgate-telegram
|
|
||||||
|
|
||||||
## 사용 전 설정
|
## 사용 전 설정
|
||||||
* `apstreams` 게시판 추가
|
* `apstreams` 게시판 추가
|
||||||
* `apstreams` 사용자 추가
|
* `apstreams` 사용자 추가
|
||||||
|
|
||||||
## 작업진행
|
## 지원현황
|
||||||
- [x] WebFinger
|
- [x] WebFinger
|
||||||
- [x] User
|
- [x] User
|
||||||
- [x] Inbox
|
- [x] Inbox
|
||||||
|
@ -24,18 +16,24 @@ ActivityPub implementation for GNUBOARD 5
|
||||||
- [x] Followers
|
- [x] Followers
|
||||||
- [x] Following
|
- [x] Following
|
||||||
- [x] Liked
|
- [x] Liked
|
||||||
- [x] (Added) Geolocation
|
- [ ] ~~Shares~~ (Altered to inbound/outbound)
|
||||||
- [x] (Added) File attachment
|
- [x] Geolocation
|
||||||
|
- [x] File attachment
|
||||||
|
- [ ] File attachment - Automatically download a remote file to the local server
|
||||||
|
- [x] Digest/Signature - Outbound
|
||||||
|
- [ ] ~~Digest/Signature - Inbound~~ (No required)
|
||||||
|
- [x] w3id.org (e.g. the `publicKey` field of an actor)
|
||||||
|
- [ ] OAuth 2.0
|
||||||
|
- [ ] Message Queue Compatible (e.g. Redis, RebbitMQ, Kafka)
|
||||||
|
|
||||||
## 부가기능 (옵션)
|
## 부가기능 (옵션)
|
||||||
|
- [x] 아바타 (gravatar.com)
|
||||||
- [x] 날씨 (openweathermap.org)
|
- [x] 날씨 (openweathermap.org)
|
||||||
- [x] 환율 (koreaexim.go.kr)
|
- [x] 환율 (koreaexim.go.kr)
|
||||||
|
- [x] 국내 Geolocation (Naver Cloud)
|
||||||
|
- [x] 국외 Geolocation (IP2Location)
|
||||||
|
|
||||||
## 향후 지원 예정
|
## 전문(메시지) 예시
|
||||||
- [ ] w3id.org 표준 지원
|
|
||||||
- [ ] 메시지 큐(Redis, RebbitMQ, Kafka 등) 지원
|
|
||||||
|
|
||||||
## 전문 예시
|
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
@ -113,5 +111,21 @@ ActivityPub implementation for GNUBOARD 5
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## References
|
||||||
|
* https://www.w3.org/TR/activitypub/
|
||||||
|
* https://www.w3.org/TR/activitystreams-core/
|
||||||
|
* https://www.w3.org/TR/activitystreams-vocabulary/
|
||||||
|
* https://github.com/w3c/activitypub/issues/194
|
||||||
|
* https://docs.joinmastodon.org/spec/webfinger/
|
||||||
|
* https://organicdesign.nz/ActivityPub_Code
|
||||||
|
* https://socialhub.activitypub.rocks/t/posting-to-pleroma-inbox/1184
|
||||||
|
* https://github.com/broidHQ/integrations/tree/master/broid-schemas#readme
|
||||||
|
* https://github.com/autogestion/pubgate-telegram
|
||||||
|
* https://docs.joinmastodon.org/spec/security/
|
||||||
|
* https://chat.openai.com/share/4fda7974-cc0b-439a-b0f2-dc828f8acfef
|
||||||
|
* https://codeberg.org/mro/activitypub/src/commit/4b1319d5363f4a836f23c784ef780b81bc674013/like.sh#L101
|
||||||
|
* https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/10
|
||||||
|
|
||||||
## 문의
|
## 문의
|
||||||
* gnh1201@gmail.com
|
* abuse@catswords.net
|
||||||
|
* ActivityPub [@gnh1201@catswords.social](https://catswords.social/@gnh1201)
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
<?php
|
<?php
|
||||||
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
||||||
|
|
||||||
// ActivityPub implementation for GNUBOARD 5
|
// Description: ActivityPub implementation for GNUBOARD 5
|
||||||
// Go Namhyeon <gnh1201@gmail.com>
|
// Author: Go Namhyeon (Catswords Research) <abuse@catswords.net>
|
||||||
// MIT License
|
// ActivityPub: @gnh1201@catswords.social
|
||||||
// 2022-07-06 (version 0.1.13)
|
// License: MIT
|
||||||
|
// Date: 2023-08-08
|
||||||
|
// Version: 0.1.18
|
||||||
// References:
|
// References:
|
||||||
// * https://www.w3.org/TR/activitypub/
|
// * https://www.w3.org/TR/activitypub/
|
||||||
// * https://www.w3.org/TR/activitystreams-core/
|
// * https://www.w3.org/TR/activitystreams-core/
|
||||||
|
@ -16,19 +17,31 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
||||||
// * https://socialhub.activitypub.rocks/t/posting-to-pleroma-inbox/1184
|
// * https://socialhub.activitypub.rocks/t/posting-to-pleroma-inbox/1184
|
||||||
// * https://github.com/broidHQ/integrations/tree/master/broid-schemas#readme
|
// * https://github.com/broidHQ/integrations/tree/master/broid-schemas#readme
|
||||||
// * https://github.com/autogestion/pubgate-telegram
|
// * https://github.com/autogestion/pubgate-telegram
|
||||||
|
// * https://blog.joinmastodon.org/2018/06/how-to-implement-a-basic-activitypub-server/
|
||||||
|
// * https://chat.openai.com/share/4fda7974-cc0b-439a-b0f2-dc828f8acfef
|
||||||
|
// * https://codeberg.org/mro/activitypub/src/commit/4b1319d5363f4a836f23c784ef780b81bc674013/like.sh#L101
|
||||||
|
// * https://socialhub.activitypub.rocks/t/problems-posting-to-mastodon-inbox/801/10
|
||||||
|
|
||||||
define("ACTIVITYPUB_INSTANCE_ID", md5_file(G5_DATA_PATH . "/dbconfig.php"));
|
define("ACTIVITYPUB_INSTANCE_ID", md5_file(G5_DATA_PATH . "/dbconfig.php"));
|
||||||
define("ACTIVITYPUB_INSTANCE_VERSION", "0.1.13-dev");
|
define("ACTIVITYPUB_INSTANCE_VERSION", "0.1.18");
|
||||||
|
define("ACTIVITYPUB_DEFAULT_SCHEME", "https"); // 외부 통신용 스킴 (SSL 사용이 기본)
|
||||||
|
define("ACTIVITYPUB_INSECURE_SCHEME", "http"); // 그누보드5 ActivityPub 통신용 스킴 (SSL 사용을 하지 않을 수도 있음을 고려)
|
||||||
define("ACTIVITYPUB_HOST", (empty(G5_DOMAIN) ? $_SERVER['HTTP_HOST'] : G5_DOMAIN));
|
define("ACTIVITYPUB_HOST", (empty(G5_DOMAIN) ? $_SERVER['HTTP_HOST'] : G5_DOMAIN));
|
||||||
define("ACTIVITYPUB_URL", (empty(G5_URL) ? "http://" . ACTIVITYPUB_INSTANCE_ID . ".local" : G5_URL));
|
define("ACTIVITYPUB_URL", (empty(G5_URL) ? ACTIVITYPUB_INSECURE_SCHEME . "://" . ACTIVITYPUB_INSTANCE_ID . ".local" : G5_URL));
|
||||||
define("ACTIVITYPUB_DATA_URL", ACTIVITYPUB_URL . '/' . G5_DATA_DIR);
|
define("ACTIVITYPUB_DATA_URL", ACTIVITYPUB_URL . '/' . G5_DATA_DIR);
|
||||||
define("ACTIVITYPUB_G5_BOARDNAME", "apstreams");
|
define("ACTIVITYPUB_G5_BOARDNAME", "apstreams");
|
||||||
define("ACTIVITYPUB_G5_TABLENAME", $g5['write_prefix'] . ACTIVITYPUB_G5_BOARDNAME);
|
define("ACTIVITYPUB_G5_TABLENAME", $g5['write_prefix'] . ACTIVITYPUB_G5_BOARDNAME);
|
||||||
define("ACTIVITYPUB_G5_USERNAME", "apstreams");
|
define("ACTIVITYPUB_G5_USERNAME", "apstreams");
|
||||||
define("ACTIVITYPUB_G5_NEW_DAYS", (empty($config['cf_new_del']) ? 30 : $config['cf_new_del']));
|
define("ACTIVITYPUB_G5_OUTDATED_DAYS", (empty($config['cf_new_del']) ? 30 : $config['cf_new_del']));
|
||||||
define("ACTIVITYPUB_ACCESS_TOKEN", "server1.example.org=xxuPtHDkMgYQfcy9; server2.example.org=PC6ujkjQXhL6lUtS;");
|
define("ACTIVITYPUB_G5_EXPIRED_DAYS", (empty($config['cf_memo_del']) ? 180 : $config['cf_memo_del']));
|
||||||
|
define("ACTIVITYPUB_ACCESS_TOKEN", "server1.example.org=YOUR_ACCESS_TOKEN; server2.example.org=YOUR_ACCESS_TOKEN;");
|
||||||
|
define("ACTIVITYPUB_CERTIFICATE_RETRY", 10); // 최대 인증서 생성 시도 횟수
|
||||||
|
define("ACTIVITYPUB_CERTIFICATE_DATAFIELD", "mb_9"); // 회원별 인증서(공개키, 개인키)를 저장할 필드 (기본: mb_9)
|
||||||
|
define("OAUTH2_GRANT_DATAFIELD", "mb_10"); // 회원별 인증 정보를 저장할 필드 (기본: mb_10)
|
||||||
|
define("DEFAULT_HTML_ENTITY_FLAGS", ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
|
||||||
define("NAMESPACE_ACTIVITYSTREAMS", "https://www.w3.org/ns/activitystreams");
|
define("NAMESPACE_ACTIVITYSTREAMS", "https://www.w3.org/ns/activitystreams");
|
||||||
define("NAMESPACE_ACTIVITYSTREAMS_PUBLIC", "https://www.w3.org/ns/activitystreams#Public");
|
define("NAMESPACE_ACTIVITYSTREAMS_PUBLIC", "https://www.w3.org/ns/activitystreams#Public");
|
||||||
|
define("NAMESPACE_W3ID_SECURITY_V1", "https://w3id.org/security/v1");
|
||||||
define("ACTIVITYPUB_ENABLED_GEOLOCATION", false); // 위치정보 활성화 (https://lite.ip2location.com/)
|
define("ACTIVITYPUB_ENABLED_GEOLOCATION", false); // 위치정보 활성화 (https://lite.ip2location.com/)
|
||||||
define("NAVERCLOUD_ENABLED_GEOLOCATION", false); // 국내용 위치정보 활성화 (https://www.ncloud.com/product/applicationService/geoLocation)
|
define("NAVERCLOUD_ENABLED_GEOLOCATION", false); // 국내용 위치정보 활성화 (https://www.ncloud.com/product/applicationService/geoLocation)
|
||||||
define("NAVERCLOUD_API_ACCESS_KEY", ""); // 네이버 클라우드 API 키 설정
|
define("NAVERCLOUD_API_ACCESS_KEY", ""); // 네이버 클라우드 API 키 설정
|
||||||
|
@ -61,6 +74,89 @@ function activitypub_load_library($name, $callback) {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ActivityPub 표준 문서에서는 단수형, 실제 어플리케이션에선 복수형으로 표현되는 것을 모두 대응함
|
||||||
|
function activitypub_cast_to_array($s) {
|
||||||
|
$d = array();
|
||||||
|
if (is_array($s)) {
|
||||||
|
$d = $s;
|
||||||
|
} else if (isset($s)) {
|
||||||
|
$d[] = $s;
|
||||||
|
}
|
||||||
|
return $d;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activitypub_create_keypair() {
|
||||||
|
$keypair = array('', '');
|
||||||
|
|
||||||
|
$privateKeyResource = openssl_pkey_new(array(
|
||||||
|
'private_key_bits' => 2048,
|
||||||
|
'private_key_type' => OPENSSL_KEYTYPE_RSA
|
||||||
|
));
|
||||||
|
|
||||||
|
// Export the private key
|
||||||
|
openssl_pkey_export($privateKeyResource, $privateKey);
|
||||||
|
|
||||||
|
// Generate the public key for the private key
|
||||||
|
$privateKeyDetailsArray = openssl_pkey_get_details($privateKeyResource);
|
||||||
|
$publicKey = $privateKeyDetailsArray['key'];
|
||||||
|
|
||||||
|
// Export keys to variable
|
||||||
|
$keypair = array($privateKey, $publicKey);
|
||||||
|
|
||||||
|
// Free the key from memory.
|
||||||
|
openssl_free_key($privateKeyResource);
|
||||||
|
|
||||||
|
return $keypair;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activitypub_get_stored_keypair($mb) {
|
||||||
|
global $g5;
|
||||||
|
|
||||||
|
$private_key = '';
|
||||||
|
$public_key = '';
|
||||||
|
|
||||||
|
// 인증서 생성
|
||||||
|
$k = 0;
|
||||||
|
while ($k < ACTIVITYPUB_CERTIFICATE_RETRY && (empty($private_key) || empty($public_key))) {
|
||||||
|
// 인증서 정보 불러오기
|
||||||
|
if ($mb != null && !empty($mb['mb_id'])) {
|
||||||
|
$certificate_data = activitypub_parse_stored_data($mb[ACTIVITYPUB_CERTIFICATE_DATAFIELD]);
|
||||||
|
$private_key = activitypub_get_memo($certificate_data['PrivateKeyId']); // 개인키(Private Key)
|
||||||
|
$public_key = activitypub_get_memo($certificate_data['PublicKeyId']); // 공개키(Public Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 인증서 정보가 없으면 생성
|
||||||
|
if (!$mb[ACTIVITYPUB_CERTIFICATE_DATAFIELD] || empty($private_key) || empty($public_key)) {
|
||||||
|
$keypair = activitypub_create_keypair(); // 인증서(공개키, 개인키) 생성
|
||||||
|
$private_key_id = activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], $keypair[0]); // 개인키(Private Key)
|
||||||
|
$public_key_id = activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], $keypair[1]); // 공개키(Public Key)
|
||||||
|
|
||||||
|
// 회원 정보에 등록
|
||||||
|
if ($private_key_id > 0 && $public_key_id > 0) {
|
||||||
|
$stored_certificate_data = activitypub_build_stored_data(array(
|
||||||
|
"PrivateKeyId" => $private_key_id,
|
||||||
|
"PublicKeyId" => $public_key_id
|
||||||
|
));
|
||||||
|
$sql = " update {$g5['member_table']} set " . ACTIVITYPUB_CERTIFICATE_DATAFIELD . " = '{$stored_certificate_data}' where mb_id = '{$mb['mb_id']}' ";
|
||||||
|
sql_query($sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 회원에게 알림
|
||||||
|
$messsge1 = "[시스템] 외부 서버와 통신하기 위한 인증서(개인키, 공개키)가 발급되었습니다. 인증서를 타인과 공유하지 마세요. 인증서는 " . ACTIVITYPUB_G5_EXPIRED_DAYS . "일 후 만료됩니다.";
|
||||||
|
$message2 = "[System] The certificate (Private key, Public key) has been issued to communicate with an external server. Do not share it with others. The certificate will expire in " . ACTIVITYPUB_G5_EXPIRED_DAYS . " days.";
|
||||||
|
activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], $messsge1 . ' ' . $message2);
|
||||||
|
|
||||||
|
// 회원정보 다시 불러오기
|
||||||
|
$mb = get_member($mb['mb_id']);
|
||||||
|
|
||||||
|
// 시도 횟수 증가
|
||||||
|
$k++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array($private_key, $public_key);
|
||||||
|
}
|
||||||
|
|
||||||
function activitypub_get_library_data($name) {
|
function activitypub_get_library_data($name) {
|
||||||
global $activitypub_loaded_libraries;
|
global $activitypub_loaded_libraries;
|
||||||
|
|
||||||
|
@ -80,21 +176,33 @@ function activitypub_json_encode($arr) {
|
||||||
return json_encode($arr);
|
return json_encode($arr);
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_json_decode($arr) {
|
function activitypub_json_decode($str) {
|
||||||
return json_decode($arr, true);
|
return json_decode($str, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_get_access_tokens() {
|
function activitypub_parse_stored_data($s) {
|
||||||
$_access_tokens = array();
|
$data = array();
|
||||||
|
|
||||||
$terms = array_filter(array_map("trim", explode(";", ACTIVITYPUB_ACCESS_TOKEN)));
|
$terms = array_filter(array_map("trim", explode(";", $s)));
|
||||||
foreach($terms as $term) {
|
foreach($terms as $term) {
|
||||||
list($k, $v) = explode('=', $term);
|
list($k, $v) = explode('=', $term);
|
||||||
$_access_tokens[$k] = $v;
|
$k = html_entity_decode($k, DEFAULT_HTML_ENTITY_FLAGS, 'UTF-8');
|
||||||
|
$v = html_entity_decode($v, DEFAULT_HTML_ENTITY_FLAGS, 'UTF-8');
|
||||||
|
$data[$k] = $v;
|
||||||
}
|
}
|
||||||
|
|
||||||
return $_access_tokens;
|
return $data;
|
||||||
};
|
}
|
||||||
|
|
||||||
|
function activitypub_build_stored_data($data) {
|
||||||
|
$terms = array();
|
||||||
|
foreach($data as $k=>$v) {
|
||||||
|
$k = htmlentities($k, DEFAULT_HTML_ENTITY_FLAGS, 'UTF-8');
|
||||||
|
$v = htmlentities($v, DEFAULT_HTML_ENTITY_FLAGS, 'UTF-8');
|
||||||
|
array_push($terms, $k . '=' . $v);
|
||||||
|
}
|
||||||
|
return implode("; ", $terms);
|
||||||
|
}
|
||||||
|
|
||||||
function activitypub_get_url($action, $params = array()) {
|
function activitypub_get_url($action, $params = array()) {
|
||||||
if (count(array_keys($params)) > 0) {
|
if (count(array_keys($params)) > 0) {
|
||||||
|
@ -122,7 +230,13 @@ function activitypub_get_icon($mb) {
|
||||||
$icon_file_url = "https://www.gravatar.com/avatar/" . md5($mb['mb_email']);
|
$icon_file_url = "https://www.gravatar.com/avatar/" . md5($mb['mb_email']);
|
||||||
}
|
}
|
||||||
|
|
||||||
return $icon_file_url;
|
$icon_ctx = array(
|
||||||
|
"type" => "Image",
|
||||||
|
"mediaType" => "image/png",
|
||||||
|
"url" => $icon_file_url
|
||||||
|
);
|
||||||
|
|
||||||
|
return $icon_ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_get_user_interactions() {
|
function activitypub_get_user_interactions() {
|
||||||
|
@ -205,10 +319,17 @@ function activitypub_add_memo($mb_id, $recv_mb_id, $me_memo) {
|
||||||
$tmp_row = sql_fetch(" select max(me_id) as max_me_id from {$g5['memo_table']} ");
|
$tmp_row = sql_fetch(" select max(me_id) as max_me_id from {$g5['memo_table']} ");
|
||||||
$me_id = $tmp_row['max_me_id'] + 1;
|
$me_id = $tmp_row['max_me_id'] + 1;
|
||||||
|
|
||||||
$sql = " insert into {$g5['memo_table']} ( me_recv_mb_id, me_send_mb_id, me_send_datetime, me_memo, me_read_datetime, me_type, me_send_ip ) values ( '$recv_mb_id', '$mb_id', '".G5_TIME_YMDHIS."', '$me_memo', '0000-00-00 00:00:00' , 'recv', '{$_SERVER['REMOTE_ADDR']}' ) ";
|
$sql = " insert into {$g5['memo_table']} ( me_recv_mb_id, me_send_mb_id, me_send_datetime, me_memo, me_read_datetime, me_type, me_send_ip ) values ( '{$recv_mb_id}', '{$mb_id}', '" . G5_TIME_YMDHIS . "', '{$me_memo}', '0000-00-00 00:00:00' , 'recv', '{$_SERVER['REMOTE_ADDR']}' ) ";
|
||||||
sql_query($sql);
|
sql_query($sql);
|
||||||
|
|
||||||
return ($me_id == sql_insert_id());
|
return ($me_id == sql_insert_id() ? $me_id : 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activitypub_get_memo($me_id) {
|
||||||
|
global $g5;
|
||||||
|
|
||||||
|
$me = sql_fetch(" select me_memo from {$g5['memo_table']} where me_id = '{$me_id}' ");
|
||||||
|
return $me['me_memo'];
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_set_liked($good, $bo_table, $wr_id) {
|
function activitypub_set_liked($good, $bo_table, $wr_id) {
|
||||||
|
@ -229,12 +350,69 @@ function activitypub_build_http_headers($headers) {
|
||||||
return $lines;
|
return $lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function activitypub_build_datetime($s='now') {
|
||||||
|
// e.g. 18 Dec 2019 10:08:46 GMT
|
||||||
|
$format = "D, d M Y H:i:s e";
|
||||||
|
$dt = ($s == "now" ? new DateTime('now', new DateTimeZone("GMT")) : DateTime::createFromFormat($format, $s));
|
||||||
|
return $dt->format($format);
|
||||||
|
}
|
||||||
|
|
||||||
|
function activitypub_build_digest($body) {
|
||||||
|
$digest = hash('sha256', $body, true);
|
||||||
|
$digest = base64_encode($digest);
|
||||||
|
$digest = 'sha-256=' . $digest;
|
||||||
|
return $digest;
|
||||||
|
}
|
||||||
|
|
||||||
|
function activitypub_build_signature($url, $date, $digest, $mb, $method="post") {
|
||||||
|
// get a certificate
|
||||||
|
list($private_key, $public_key) = activitypub_get_stored_keypair($mb);
|
||||||
|
|
||||||
|
// get host and path from URL
|
||||||
|
list($host, $path) = array(parse_url($url, PHP_URL_HOST), parse_url($url, PHP_URL_PATH));
|
||||||
|
|
||||||
|
// build a key id
|
||||||
|
$activitypub_user_id = activitypub_get_url("user", array("mb_id" => $mb['mb_id']));
|
||||||
|
$keyId = $activitypub_user_id . "#main-key";
|
||||||
|
|
||||||
|
// build a target data to get signature
|
||||||
|
/*
|
||||||
|
$signature = $method . ' ' . $path . "\n" .
|
||||||
|
'HOST: ' . $host . "\n" .
|
||||||
|
'Date: ' . $date . "\n" .
|
||||||
|
'Digest: ' . $digest;
|
||||||
|
*/
|
||||||
|
// Ref: https://codeberg.org/mro/activitypub/src/commit/4b1319d5363f4a836f23c784ef780b81bc674013/like.sh#L101
|
||||||
|
$signature = sprintf(
|
||||||
|
"%s: %s\n%s: %s\n%s: %s\n%s: %s",
|
||||||
|
"(request-target)",
|
||||||
|
"{$method} {$path}",
|
||||||
|
"host", $host,
|
||||||
|
"date", $date,
|
||||||
|
"digest", $digest
|
||||||
|
);
|
||||||
|
|
||||||
|
// create a signature
|
||||||
|
openssl_sign($signature, $signature, $private_key, OPENSSL_ALGO_SHA256);
|
||||||
|
$signature = base64_encode($signature);
|
||||||
|
|
||||||
|
// create a signature header
|
||||||
|
return 'keyId="' . $keyId . '",headers="(request-target) host date digest",signature="' . $signature . '"';
|
||||||
|
}
|
||||||
|
|
||||||
function activitypub_http_get($url, $access_token = '') {
|
function activitypub_http_get($url, $access_token = '') {
|
||||||
$headers = array("Accept" => "application/ld+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\"");
|
// build the header
|
||||||
|
$headers = array(
|
||||||
|
"Date" => activitypub_build_datetime('now'),
|
||||||
|
"Accept" => "application/activity+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\""
|
||||||
|
);
|
||||||
|
|
||||||
|
// set access token
|
||||||
if (!empty($access_token)) {
|
if (!empty($access_token)) {
|
||||||
$headers["Authorization"] = "Bearer " . $access_token;
|
$headers["Authorization"] = "Bearer " . $access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// request
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt_array($ch, array(
|
curl_setopt_array($ch, array(
|
||||||
CURLOPT_URL => $url,
|
CURLOPT_URL => $url,
|
||||||
|
@ -267,12 +445,28 @@ function activitypub_get_attachments($bo_table, $wr_id) {
|
||||||
return $attachments;
|
return $attachments;
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_http_post($url, $rawdata, $access_token = '') {
|
function activitypub_http_post($url, $rawdata, $mb, $access_token = '') {
|
||||||
$headers = array("Accept" => "application/ld+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\"");
|
// get digest
|
||||||
|
$date = activitypub_build_datetime('now');
|
||||||
|
$digest = activitypub_build_digest($rawdata);
|
||||||
|
|
||||||
|
// build the headers
|
||||||
|
$headers = array(
|
||||||
|
"Date" => $date,
|
||||||
|
"Digest" => $digest,
|
||||||
|
"Content-Type" => "application/activity+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\"",
|
||||||
|
);
|
||||||
|
|
||||||
|
// build the signature
|
||||||
|
$signature = activitypub_build_signature($url, $date, $digest, $mb);
|
||||||
|
$headers["Signature"] = $signature;
|
||||||
|
|
||||||
|
// set access token
|
||||||
if (!empty($access_token)) {
|
if (!empty($access_token)) {
|
||||||
$headers["Authorization"] = "Bearer " . $access_token;
|
$headers["Authorization"] = "Bearer " . $access_token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// request
|
||||||
$ch = curl_init();
|
$ch = curl_init();
|
||||||
curl_setopt_array($ch, array(
|
curl_setopt_array($ch, array(
|
||||||
CURLOPT_URL => $url,
|
CURLOPT_URL => $url,
|
||||||
|
@ -284,8 +478,14 @@ function activitypub_http_post($url, $rawdata, $access_token = '') {
|
||||||
CURLOPT_POST => true
|
CURLOPT_POST => true
|
||||||
));
|
));
|
||||||
$response = curl_exec($ch);
|
$response = curl_exec($ch);
|
||||||
|
$errno = curl_errno($ch);
|
||||||
curl_close($ch);
|
curl_close($ch);
|
||||||
|
|
||||||
|
// 전송 오류가 있었을 시 쪽지로 알림
|
||||||
|
if ($errno) {
|
||||||
|
activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], "[경고] 메시지 전송 중 오류가 발생함. 오류 번호: " . $errno);
|
||||||
|
}
|
||||||
|
|
||||||
return activitypub_json_decode($response, true);
|
return activitypub_json_decode($response, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -381,6 +581,9 @@ function koreaexim_get_exchange_data() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_publish_content($content, $object_id, $mb, $_added_object = array(), $_added_to = array()) {
|
function activitypub_publish_content($content, $object_id, $mb, $_added_object = array(), $_added_to = array()) {
|
||||||
|
// 액티비티 관리용 계정의 글 전송 차단
|
||||||
|
if ($mb['mb_id'] == ACTIVITYPUB_G5_USERNAME) return;
|
||||||
|
|
||||||
// 위치정보를 사용하는 경우 모듈 로드
|
// 위치정보를 사용하는 경우 모듈 로드
|
||||||
$location_ctx = array();
|
$location_ctx = array();
|
||||||
if (ACTIVITYPUB_ENABLED_GEOLOCATION) {
|
if (ACTIVITYPUB_ENABLED_GEOLOCATION) {
|
||||||
|
@ -468,7 +671,10 @@ function activitypub_publish_content($content, $object_id, $mb, $_added_object =
|
||||||
$terms = activitypub_parse_content($content);
|
$terms = activitypub_parse_content($content);
|
||||||
|
|
||||||
// 수신자/내용 생성
|
// 수신자/내용 생성
|
||||||
$to = array_merge(array(NAMESPACE_ACTIVITYSTREAMS_PUBLIC), $_added_to);
|
$to = array_merge(activitypub_cast_to_array(NAMESPACE_ACTIVITYSTREAMS_PUBLIC), $_added_to);
|
||||||
|
$cc = array(); // 참조자
|
||||||
|
$tag = array(); // 태그
|
||||||
|
$endpoints = array();
|
||||||
$content = "";
|
$content = "";
|
||||||
foreach($terms as $term_ctx) {
|
foreach($terms as $term_ctx) {
|
||||||
switch ($term_ctx['type']) {
|
switch ($term_ctx['type']) {
|
||||||
|
@ -477,23 +683,45 @@ function activitypub_publish_content($content, $object_id, $mb, $_added_object =
|
||||||
$account = substr($term_ctx['value'], 1);
|
$account = substr($term_ctx['value'], 1);
|
||||||
$account_terms = explode('@', $account);
|
$account_terms = explode('@', $account);
|
||||||
$account_ctx = array("username" => $account_terms[0], "host" => $account_terms[1]);
|
$account_ctx = array("username" => $account_terms[0], "host" => $account_terms[1]);
|
||||||
|
$webfigner_ctx = array("subject" => "");
|
||||||
if (!empty($account_ctx['host'])) {
|
if (!empty($account_ctx['host'])) {
|
||||||
// 공통 WebFinger에 연결
|
$counter = 2; // WebFinger에 연결하는 경우의 수 정의 (=N-1)
|
||||||
$webfigner_ctx = activitypub_http_get("http://" . $account_ctx['host'] . "/.well-known/webfinger?resource=acct:" . $account);
|
while ($counter > -1 && $webfigner_ctx['subject'] != ("acct:" . $account)) {
|
||||||
|
switch($counter) {
|
||||||
|
case 0: // 공통 WebFinger에 연결
|
||||||
|
$webfigner_ctx = activitypub_http_get(ACTIVITYPUB_DEFAULT_SCHEME . "://" . $account_ctx['host'] . "/.well-known/webfinger?resource=acct:" . $account);
|
||||||
|
break;
|
||||||
|
case 1: // 실패시, 그누보드5용 WebFinger에 연결
|
||||||
|
$webfigner_ctx = activitypub_http_get(ACTIVITYPUB_DEFAULT_SCHEME . "://" . $account_ctx['host'] . "/?route=webfinger&resource=acct:" . $account);
|
||||||
|
break;
|
||||||
|
case 2: // 실패시, 그누보드5용 WebFinger에 연결 + 보안통신 해제
|
||||||
|
$webfigner_ctx = activitypub_http_get(ACTIVITYPUB_INSECURE_SCHEME . "://" . $account_ctx['host'] . "/?route=webfinger&resource=acct:" . $account);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$counter = -1;
|
||||||
|
}
|
||||||
|
|
||||||
// 실패시, 그누5 전용 WebFinger에 연결
|
$counter--; // 시도 횟수 차감
|
||||||
if ($webfigner_ctx['subject'] != ("acct:" . $account)) {
|
|
||||||
$webfigner_ctx = activitypub_http_get("http://" . $account_ctx['host'] . "/?route=webfinger&resource=acct:" . $account);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 한번 더 확인
|
// WebFinger 정보 수신을 못한 경우, 쪽지로 알리고 아무 작업도 하지 않음
|
||||||
if ($webfigner_ctx['subject'] != ("acct:" . $account)) break;
|
if (empty($webfigner_ctx['subject'])) {
|
||||||
|
activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], "[발송실패] 수신자를 찾을 수 없음: @" . $account);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
// 받은 요청으로 처리
|
// 받은 요청으로 처리
|
||||||
$webfigner_links = $webfigner_ctx['links'];
|
$webfigner_links = $webfigner_ctx['links'];
|
||||||
foreach($webfigner_links as $link) {
|
foreach($webfigner_links as $link) {
|
||||||
if ($link['rel'] == "self" && $link['type'] == "application/activity+json") {
|
if ($link['rel'] == "self" && $link['type'] == "application/activity+json") {
|
||||||
array_push($to, $link['href']); // 수신자에 반영
|
// 태그 목록에 추가
|
||||||
|
array_push($tag, array(
|
||||||
|
"type" => "Mention",
|
||||||
|
"href" => $link['href'],
|
||||||
|
"name" => '@' . $account
|
||||||
|
));
|
||||||
|
array_push($cc, $link['href']); // 참조자 목록에 추가
|
||||||
|
array_push($endpoints, $link['href']); // 글을 전송할 엔드포인트(URL)에 반영
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -505,67 +733,66 @@ function activitypub_publish_content($content, $object_id, $mb, $_added_object =
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전문 생성 시작
|
|
||||||
$object = array(
|
|
||||||
"type" => "Note",
|
|
||||||
"generator" => "GNUBOARD5 ActivityPub Plugin (INSTANCE_ID: " . ACTIVITYPUB_INSTANCE_ID . ", INSTANCE_VERSION: " . ACTIVITYPUB_INSTANCE_VERSION . ")",
|
|
||||||
"id" => $object_id,
|
|
||||||
"attributedTo" => activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
|
|
||||||
"content" => $content,
|
|
||||||
"icon" => activitypub_get_icon($mb)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 위치정보가 활성화되어 있으면
|
// 위치정보가 활성화되어 있으면
|
||||||
if (ACTIVITYPUB_ENABLED_GEOLOCATION) {
|
if (ACTIVITYPUB_ENABLED_GEOLOCATION) {
|
||||||
$object = array_merge($object, array(
|
$_added_object = array_merge($_added_object, array(
|
||||||
"location" => $location_ctx
|
"location" => $location_ctx
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전문 생성 완료
|
// 태그 추가
|
||||||
$object = array_merge($object, $_added_object);
|
$_added_object = array_merge($_added_object, array(
|
||||||
|
"tag" => $tag
|
||||||
|
));
|
||||||
|
|
||||||
|
// 전문 생성
|
||||||
|
$object = activitypub_build_note($content, $object_id, $mb, $_added_object);
|
||||||
|
|
||||||
// 외부로 보낼 전문 생성
|
// 외부로 보낼 전문 생성
|
||||||
$data = array(
|
$data = array(
|
||||||
"@context" => NAMESPACE_ACTIVITYSTREAMS,
|
"@context" => activitypub_cast_to_array(NAMESPACE_ACTIVITYSTREAMS),
|
||||||
"type" => "Create",
|
"type" => "Create",
|
||||||
"id" => G5_BBS_URL . "/board.php?bo_table=" . ACTIVITYPUB_G5_BOARDNAME . "#Draft",
|
"id" => G5_BBS_URL . "/board.php?bo_table=" . ACTIVITYPUB_G5_BOARDNAME . "#Draft",
|
||||||
"to" => $to,
|
"to" => $to,
|
||||||
|
"cc" => $cc,
|
||||||
"actor" => $object['attributedTo'],
|
"actor" => $object['attributedTo'],
|
||||||
"object" => $object
|
"object" => $object
|
||||||
);
|
);
|
||||||
|
|
||||||
// 초안(Draft) 작성
|
// 초안(Draft) 작성
|
||||||
$activity_wr_id = activitypub_update_activity("outbox", $data, $mb, "draft");
|
$activity_id = activitypub_update_activity("outbox", $data, $mb, "draft");
|
||||||
$data['id'] = G5_BBS_URL . "/board.php?bo_table=" . ACTIVITYPUB_G5_BOARDNAME . "&wr_id=" . $activity_wr_id;
|
$data['object']['id'] = G5_BBS_URL . "/board.php?bo_table=" . ACTIVITYPUB_G5_BOARDNAME . "&wr_id=" . $activity_id;
|
||||||
|
$data['id'] = activitypub_get_url("activity", array("id" => $activity_id));
|
||||||
|
|
||||||
|
// 현재 시간 반영
|
||||||
|
$now_utc_tz = str_replace('+00:00', 'Z', gmdate('c'));
|
||||||
|
$data['published'] = $now_utc_tz;
|
||||||
|
$data['updated'] = $now_utc_tz;
|
||||||
|
|
||||||
// 보낼 전문을 인코딩
|
// 보낼 전문을 인코딩
|
||||||
$rawdata = activitypub_json_encode($data);
|
$rawdata = activitypub_json_encode($data);
|
||||||
|
|
||||||
// 수신자 작업
|
// 수신자 엔드포인트(URL) 작업
|
||||||
foreach($to as $_to) {
|
foreach($endpoints as $endpoint) {
|
||||||
// 공개 네임스페이스인 경우 건너뛰기
|
|
||||||
if ($_to == NAMESPACE_ACTIVITYSTREAMS_PUBLIC) continue;
|
|
||||||
|
|
||||||
// 수신자 정보 조회
|
// 수신자 정보 조회
|
||||||
$remote_user_ctx = activitypub_http_get($_to);
|
$remote_account_ctx = activitypub_http_get($endpoint);
|
||||||
|
|
||||||
// inbox 주소 찾기
|
// inbox 주소 찾기
|
||||||
$remote_inbox_url = $remote_user_ctx['inbox'];
|
$remote_inbox_url = $remote_account_ctx['inbox'];
|
||||||
if (empty($remote_inbox_url)) {
|
if (empty($remote_inbox_url)) {
|
||||||
$remote_inbox_url = $remote_user_ctx['endpoints']['sharedInbox'];
|
$remote_inbox_url = $remote_account_ctx['endpoints']['sharedInbox'];
|
||||||
}
|
}
|
||||||
|
|
||||||
// inbox 주소가 없으면 건너뛰기
|
// inbox 주소가 없으면 건너뛰기
|
||||||
if (empty($remote_inbox_url)) {
|
if (empty($remote_inbox_url)) {
|
||||||
activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], "Could not find the inbox of " . $_to);
|
activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], "이 사용자 또는 서버는 메시지를 수신할 수 없는 상태임: " . $_to);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 엑세스 토큰(Access Token)이 존재하는 목적지인 경우
|
// 엑세스 토큰(Access Token)이 존재하는 목적지인 경우
|
||||||
$access_token = '';
|
$access_token = '';
|
||||||
$_access_tokens = activitypub_get_access_tokens();
|
$access_token_data = activitypub_parse_stored_data(ACTIVITYPUB_ACCESS_TOKEN);
|
||||||
foreach($_access_tokens as $k=>$v) {
|
foreach($access_token_data as $k=>$v) {
|
||||||
if(strpos($_to, "http://" . $k . "/") !== false || strpos($_to, "https://" . $k . "/") !== false) {
|
if(strpos($_to, "http://" . $k . "/") !== false || strpos($_to, "https://" . $k . "/") !== false) {
|
||||||
$access_token = $v;
|
$access_token = $v;
|
||||||
break;
|
break;
|
||||||
|
@ -573,7 +800,7 @@ function activitypub_publish_content($content, $object_id, $mb, $_added_object =
|
||||||
}
|
}
|
||||||
|
|
||||||
// inbox로 데이터 전송
|
// inbox로 데이터 전송
|
||||||
$response = activitypub_http_post($remote_inbox_url, $rawdata, $access_token);
|
activitypub_http_post($remote_inbox_url, $rawdata, $mb, $access_token);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 발행됨(Published)으로 상태 업데이트
|
// 발행됨(Published)으로 상태 업데이트
|
||||||
|
@ -588,12 +815,13 @@ function activitypub_parse_content($content) {
|
||||||
$pos = -1;
|
$pos = -1;
|
||||||
$get_next_position = function ($pos) use ($content) {
|
$get_next_position = function ($pos) use ($content) {
|
||||||
try {
|
try {
|
||||||
return min(array_filter(array(
|
$positions = array_filter(array(
|
||||||
strpos($content, '@', $pos + 1),
|
strpos($content, '@', $pos + 1),
|
||||||
strpos($content, '#', $pos + 1),
|
strpos($content, '#', $pos + 1),
|
||||||
strpos($content, 'http://', $pos + 1),
|
strpos($content, 'http://', $pos + 1),
|
||||||
strpos($content, 'https://', $pos + 1)
|
strpos($content, 'https://', $pos + 1)
|
||||||
), "is_numeric"));
|
), "is_numeric");
|
||||||
|
return (count($positions) > 0 ? min($positions) : false);
|
||||||
} catch (ValueError $e) {
|
} catch (ValueError $e) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -650,9 +878,9 @@ function activitypub_update_activity($inbox = "inbox", $data, $mb = array("mb_id
|
||||||
$wr_num = get_next_num($write_table);
|
$wr_num = get_next_num($write_table);
|
||||||
$wr_reply = '';
|
$wr_reply = '';
|
||||||
$ca_name = $inbox; // Inbox/Outbox
|
$ca_name = $inbox; // Inbox/Outbox
|
||||||
$wr_subject = mb_substr($content, 0, 50);
|
$wr_subject = mb_substr(strip_tags($content), 0, 50);
|
||||||
$wr_seo_title = $content;
|
$wr_seo_title = strip_tags($content);
|
||||||
$wr_content = $content . "\r\n\r\n[외부에서 전송된 글입니다.]";
|
$wr_content = strip_tags($content) . "\r\n\r\n[외부에서 전송된 글입니다.]";
|
||||||
$wr_link1 = $data['actor'];
|
$wr_link1 = $data['actor'];
|
||||||
$wr_link2 = '';
|
$wr_link2 = '';
|
||||||
$wr_homepage = $data['actor'];
|
$wr_homepage = $data['actor'];
|
||||||
|
@ -723,8 +951,8 @@ function activitypub_update_activity($inbox = "inbox", $data, $mb = array("mb_id
|
||||||
if ($status == "published") {
|
if ($status == "published") {
|
||||||
// 저장 전 데이터 처리
|
// 저장 전 데이터 처리
|
||||||
$now_utc_tz = str_replace('+00:00', 'Z', gmdate('c'));
|
$now_utc_tz = str_replace('+00:00', 'Z', gmdate('c'));
|
||||||
$data['published'] = $now_utc_tz;
|
$data['published'] = empty($data['published']) ? $now_utc_tz : $data['published'];
|
||||||
$data['updated'] = $now_utc_tz;
|
$data['updated'] = empty($data['updated']) ? $now_utc_tz : $data['updated'];
|
||||||
|
|
||||||
// 요청 전문은 파일로 저장
|
// 요청 전문은 파일로 저장
|
||||||
$raw_context = activitypub_json_encode($data);
|
$raw_context = activitypub_json_encode($data);
|
||||||
|
@ -766,57 +994,60 @@ function activitypub_update_activity($inbox = "inbox", $data, $mb = array("mb_id
|
||||||
return $wr_id;
|
return $wr_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
function activitypub_get_objects($inbox = "inbox", $mb_id = '') {
|
function activitypub_get_activity_by_id($activity_id) {
|
||||||
global $g5;
|
global $g5;
|
||||||
|
|
||||||
$items = array();
|
// 액티비티 전문
|
||||||
|
$activity_ctx = array();
|
||||||
|
|
||||||
// 정보 불러오기
|
// 해당 액티비티 찾고 없으면 빈 정보 반환
|
||||||
$sql = "";
|
$write_table = $g5['write_prefix'] . ACTIVITYPUB_G5_BOARDNAME;
|
||||||
if(empty($mb_id)) {
|
$wr = get_write($write_table, $activity_id);
|
||||||
$sql = "select wr_id from " . ACTIVITYPUB_G5_TABLENAME . "
|
if (empty($wr['wr_id'])) return $activity_ctx;
|
||||||
where ca_name = '$inbox'
|
|
||||||
and DATE(wr_datetime) BETWEEN CURDATE() - INTERVAL " . ACTIVITYPUB_G5_NEW_DAYS . " DAY AND CURDATE()
|
// 액티비티 조회
|
||||||
";
|
$sql = "select * from {$g5['board_file_table']}
|
||||||
} else {
|
where bo_table = '" . ACTIVITYPUB_G5_BOARDNAME . "' and wr_id = '{$wr['wr_id']}' and bf_content = 'application/activity+json'";
|
||||||
$sql = "select wr_id from " . ACTIVITYPUB_G5_TABLENAME . "
|
|
||||||
where ca_name = '$inbox'
|
|
||||||
and FIND_IN_SET('$mb_id', wr_7) > 0
|
|
||||||
and DATE(wr_datetime) BETWEEN CURDATE() - INTERVAL " . ACTIVITYPUB_G5_NEW_DAYS . " DAY AND CURDATE()
|
|
||||||
";
|
|
||||||
}
|
|
||||||
$result = sql_query($sql);
|
$result = sql_query($sql);
|
||||||
|
|
||||||
// 정보 조회 후 처리
|
|
||||||
while ($row = sql_fetch_array($result)) {
|
while ($row = sql_fetch_array($result)) {
|
||||||
$sql2 = "select * from {$g5['board_file_table']}
|
$filename = $row['bf_file'];
|
||||||
where bo_table = '" . ACTIVITYPUB_G5_BOARDNAME . "' and wr_id = '{$row['wr_id']}' and bf_content = 'application/activity+json'";
|
$filepath = G5_DATA_PATH . "/file/" . ACTIVITYPUB_G5_BOARDNAME . "/" . $filename;
|
||||||
$result2 = sql_query($sql2);
|
if (file_exists($filepath)) {
|
||||||
while ($row2 = sql_fetch_array($result2)) {
|
$activity_ctx = activitypub_json_decode(@file_get_contents($filepath))['object'];
|
||||||
$filename = $row2['bf_file'];
|
|
||||||
$filepath = G5_DATA_PATH . "/file/" . ACTIVITYPUB_G5_BOARDNAME . "/" . $filename;
|
|
||||||
if(file_exists($filepath)) {
|
|
||||||
array_push($items, activitypub_json_decode(file_get_contents($filepath))['object']);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 전문 만들기
|
return $activity_ctx;
|
||||||
$context = array(
|
}
|
||||||
|
|
||||||
|
// Object type: Note
|
||||||
|
function activitypub_build_note($content, $object_id, $mb, $_added_object = array()) {
|
||||||
|
return array_merge(array(
|
||||||
|
"type" => "Note",
|
||||||
|
"generator" => "G5.ActivityPub/" . ACTIVITYPUB_INSTANCE_VERSION . " (GNUBOARD " . G5_GNUBOARD_VER . "; " . ACTIVITYPUB_INSTANCE_ID . ")",
|
||||||
|
"id" => $object_id,
|
||||||
|
"attributedTo" => activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
|
||||||
|
"content" => $content,
|
||||||
|
"icon" => activitypub_get_icon($mb)
|
||||||
|
), $_added_object);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object type: Collection
|
||||||
|
function activitypub_build_collection($items, $summary = '') {
|
||||||
|
return array(
|
||||||
"@context" => NAMESPACE_ACTIVITYSTREAMS,
|
"@context" => NAMESPACE_ACTIVITYSTREAMS,
|
||||||
"summary" => "Latest items",
|
"generator" => "G5.ActivityPub/" . ACTIVITYPUB_INSTANCE_VERSION . " (GNUBOARD " . G5_GNUBOARD_VER . "; " . ACTIVITYPUB_INSTANCE_ID . ")",
|
||||||
|
"summary" => $summary,
|
||||||
"type" => "Collection",
|
"type" => "Collection",
|
||||||
"totalItems" => count($items),
|
"totalItems" => count($items),
|
||||||
"items" => $items,
|
"items" => $items,
|
||||||
"updated" => str_replace('+00:00', 'Z', gmdate('c'))
|
"updated" => str_replace('+00:00', 'Z', gmdate('c'))
|
||||||
);
|
);
|
||||||
|
|
||||||
return $context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _GNUBOARD_ActivityPub {
|
class _GNUBOARD_ActivityPub {
|
||||||
public static function open() {
|
public static function open() {
|
||||||
header("Content-Type: application/ld+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\"");
|
header("Content-Type: application/activity+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\"");
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function webfinger() {
|
public static function webfinger() {
|
||||||
|
@ -903,29 +1134,50 @@ class _GNUBOARD_ActivityPub {
|
||||||
return activitypub_json_encode(array("message" => "Could not find the user"));
|
return activitypub_json_encode(array("message" => "Could not find the user"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 인증서 정보 불러오기
|
||||||
|
list($private_key, $public_key) = activitypub_get_stored_keypair($mb);
|
||||||
|
|
||||||
|
// 본문 생성
|
||||||
|
$activitypub_user_id = activitypub_get_url("user", array("mb_id" => $mb['mb_id']));
|
||||||
$context = array(
|
$context = array(
|
||||||
"@context" => array(NAMESPACE_ACTIVITYSTREAMS, array("@language" => "ko")),
|
"@context" => array(NAMESPACE_ACTIVITYSTREAMS, NAMESPACE_W3ID_SECURITY_V1, array("@language" => "ko")),
|
||||||
"type" => "Person",
|
"type" => "Person",
|
||||||
"id" => activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
|
"id" => $activitypub_user_id,
|
||||||
"name" => $mb['mb_name'],
|
"name" => $mb['mb_nick'], // display name
|
||||||
"preferredUsername" => $mb['mb_nick'],
|
"preferredUsername" => $mb['mb_id'], // real ID
|
||||||
"summary" => $mb['mb_profile'],
|
"summary" => $mb['mb_profile'],
|
||||||
"inbox" => activitypub_get_url("inbox", array("mb_id" => $mb['mb_id'])),
|
"inbox" => activitypub_get_url("inbox", array("mb_id" => $mb['mb_id'])),
|
||||||
"outbox" => activitypub_get_url("outbox", array("mb_id" => $mb['mb_id'])),
|
"outbox" => activitypub_get_url("outbox", array("mb_id" => $mb['mb_id'])),
|
||||||
"followers" => activitypub_get_url("followers", array("mb_id" => $mb['mb_id'])),
|
"followers" => activitypub_get_url("followers", array("mb_id" => $mb['mb_id'])),
|
||||||
"following" => activitypub_get_url("following", array("mb_id" => $mb['mb_id'])),
|
"following" => activitypub_get_url("following", array("mb_id" => $mb['mb_id'])),
|
||||||
"liked" => activitypub_get_url("liked", array("mb_id" => $mb['mb_id'])),
|
"liked" => activitypub_get_url("liked", array("mb_id" => $mb['mb_id'])),
|
||||||
"icon" => array(
|
"icon" => activitypub_get_icon($mb),
|
||||||
activitypub_get_icon($mb)
|
|
||||||
),
|
|
||||||
"endpoints" => array(
|
"endpoints" => array(
|
||||||
"sharedInbox" => activitypub_get_url("inbox")
|
"sharedInbox" => activitypub_get_url("inbox")
|
||||||
)
|
),
|
||||||
|
"publicKey" => array(
|
||||||
|
"id" => $activitypub_user_id . "#main-key",
|
||||||
|
"owner" => $activitypub_user_id,
|
||||||
|
"publicKeyPem" => $public_key
|
||||||
|
),
|
||||||
|
"summary" => strip_tags($mb['mb_signature']),
|
||||||
|
"discoverable" => true,
|
||||||
);
|
);
|
||||||
|
|
||||||
return activitypub_json_encode($context);
|
return activitypub_json_encode($context);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function activity() {
|
||||||
|
// HTTP 요청 유형에 따라 작업
|
||||||
|
switch ($_SERVER['REQUEST_METHOD']) {
|
||||||
|
case "POST":
|
||||||
|
return activitypub_json_encode(array("message" => "Disallowed method"));
|
||||||
|
|
||||||
|
case "GET":
|
||||||
|
return activitypub_get_activity_by_id($_GET['id']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function inbox() {
|
public static function inbox() {
|
||||||
// HTTP 요청 유형에 따라 작업
|
// HTTP 요청 유형에 따라 작업
|
||||||
switch ($_SERVER['REQUEST_METHOD']) {
|
switch ($_SERVER['REQUEST_METHOD']) {
|
||||||
|
@ -934,11 +1186,11 @@ class _GNUBOARD_ActivityPub {
|
||||||
// 공개(Public) 설정한 메시지는 ACTIVITYPUB_G5_TABLENAME에 저장
|
// 공개(Public) 설정한 메시지는 ACTIVITYPUB_G5_TABLENAME에 저장
|
||||||
$data = activitypub_json_decode(file_get_contents("php://input"), true);
|
$data = activitypub_json_decode(file_get_contents("php://input"), true);
|
||||||
|
|
||||||
if (empty($data['@context'])) {
|
// @context의 네임스페이스는 단수형(string으로 표현) 또는 복수형(array로 표현)될 수 있음
|
||||||
return activitypub_json_encode(array("message" => "This is a broken context"));
|
$namespaces = activitypub_cast_to_array($data['@context']);
|
||||||
}
|
|
||||||
|
|
||||||
if ($data['@context'] != NAMESPACE_ACTIVITYSTREAMS) {
|
// ActivityStream 네임스페이스가 존재하지 않는 경우 요청 거절
|
||||||
|
if (!in_array(NAMESPACE_ACTIVITYSTREAMS, $namespaces)) {
|
||||||
return activitypub_json_encode(array("message" => "This is not an ActivityStreams request"));
|
return activitypub_json_encode(array("message" => "This is not an ActivityStreams request"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -950,8 +1202,11 @@ class _GNUBOARD_ActivityPub {
|
||||||
// 정보 불러오기
|
// 정보 불러오기
|
||||||
$mb = get_member(ACTIVITYPUB_G5_USERNAME);
|
$mb = get_member(ACTIVITYPUB_G5_USERNAME);
|
||||||
|
|
||||||
// 수신자 확인
|
// 수신자 (참조자 포함) 확인
|
||||||
$to = $data['to'];
|
$to = array_merge(
|
||||||
|
activitypub_cast_to_array($data['to']),
|
||||||
|
activitypub_cast_to_array($data['cc'])
|
||||||
|
);
|
||||||
|
|
||||||
// 원글 정보 확인
|
// 원글 정보 확인
|
||||||
$object = $data['object'];
|
$object = $data['object'];
|
||||||
|
@ -972,7 +1227,12 @@ class _GNUBOARD_ActivityPub {
|
||||||
|
|
||||||
// 컨텐츠 설정
|
// 컨텐츠 설정
|
||||||
$bo = get_board_db(ACTIVITYPUB_G5_BOARDNAME, true);
|
$bo = get_board_db(ACTIVITYPUB_G5_BOARDNAME, true);
|
||||||
$content = sprintf("%s\r\n\r\n[외부에서 전송된 글입니다. 자세한 내용은 %s#%s 글을 확인하세요.]", $object['content'], $bo['bo_subject'], $activity_wr_id);
|
$content = sprintf(
|
||||||
|
"%s\r\n\r\n[외부에서 전송된 글입니다. 자세한 내용은 %s#%s 글을 확인하세요.]",
|
||||||
|
strip_tags($object['content']),
|
||||||
|
$bo['bo_subject'],
|
||||||
|
$activity_wr_id
|
||||||
|
);
|
||||||
|
|
||||||
// 답글인지 확인
|
// 답글인지 확인
|
||||||
if (!empty($object['inReplyTo'])) {
|
if (!empty($object['inReplyTo'])) {
|
||||||
|
@ -1161,8 +1421,7 @@ class _GNUBOARD_ActivityPub {
|
||||||
return activitypub_json_encode(array("message" => "Success"));
|
return activitypub_json_encode(array("message" => "Success"));
|
||||||
|
|
||||||
case "GET":
|
case "GET":
|
||||||
$mb = get_member($_GET['mb_id']);
|
return activitypub_json_encode(array("message" => "Disallowed method"));
|
||||||
return activitypub_json_encode(activitypub_get_activities("inbox", $mb['mb_id']));
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return activitypub_json_encode(array("message" => "Not supported method"));
|
return activitypub_json_encode(array("message" => "Not supported method"));
|
||||||
|
@ -1181,24 +1440,66 @@ class _GNUBOARD_ActivityPub {
|
||||||
// 가장 최근의 활동을 가져옴
|
// 가장 최근의 활동을 가져옴
|
||||||
case "GET":
|
case "GET":
|
||||||
$mb = get_member($_GET['mb_id']);
|
$mb = get_member($_GET['mb_id']);
|
||||||
return activitypub_json_encode(activitypub_get_activities("outbox", $mb['mb_id']));
|
return activitypub_json_encode(activitypub_get_objects($mb, "outbox"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function followers() {
|
public static function followers() {
|
||||||
$mb = get_member($_GET['mb_id']);
|
$mb = get_member($_GET['mb_id']);
|
||||||
return activitypub_json_encode(array("followers" => activitypub_get_followers($mb)));
|
return activitypub_json_encode(activitypub_build_collection(activitypub_get_followers($mb), "{$mb['mb_name']}'s followers"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function following() {
|
public static function following() {
|
||||||
$mb = get_member($_GET['mb_id']);
|
$mb = get_member($_GET['mb_id']);
|
||||||
return activitypub_json_encode(array("following" => activitypub_get_following($mb)));
|
return activitypub_json_encode(activitypub_build_collection(activitypub_get_following($mb), "{$mb['mb_name']}'s following"));
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function liked() {
|
public static function liked() {
|
||||||
return self::inbox();
|
return self::inbox();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function shares() {
|
||||||
|
global $g5;
|
||||||
|
|
||||||
|
$items = array(); // 항목을 담을 배열
|
||||||
|
|
||||||
|
// 최근 활동에서 추출
|
||||||
|
$sql = "select * from " . $g5['board_new_table'];
|
||||||
|
$result = sql_query($sql);
|
||||||
|
while ($row = sql_fetch_array($result)) {
|
||||||
|
$write_table = $g5['write_prefix'] . $row['bo_table'];
|
||||||
|
$sql2 = "select wr_id, mb_id, wr_content, wr_datetime from {$write_table} where wr_id = '{$row['wr_id']}' and FIND_IN_SET('secret', wr_option) = 0 ";
|
||||||
|
$row2 = sql_fetch($sql2);
|
||||||
|
if ($row2['wr_id']) {
|
||||||
|
$object_id = G5_BBS_URL . "/board.php?bo_table={$row['bo_table']}&wr_id={$row2['wr_id']}";
|
||||||
|
$mb = get_member($row2['mb_id']);
|
||||||
|
$content = $row2['wr_content'];
|
||||||
|
array_push($items, activitypub_build_note($content, $object_id, $mb));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 결과 반환
|
||||||
|
return activitypub_json_encode(activitypub_build_collection($items, "Latest shares"));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function authorize() {
|
||||||
|
$grant_type = $_GET['grant_type'];
|
||||||
|
|
||||||
|
switch ($grant_type) {
|
||||||
|
case "authorization_code":
|
||||||
|
return activitypub_json_encode(array("message" => "Not implemented"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "password":
|
||||||
|
return activitypub_json_encode(array("message" => "Not implemented"));
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "client_credentials":
|
||||||
|
return activitypub_json_encode(array("message" => "Not implemented"));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function close() {
|
public static function close() {
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
@ -1211,13 +1512,13 @@ function _activitypub_memo_form_update_after($member_list, $str_nick_list, $redi
|
||||||
// 'apstreams' 계정이 있는지 확인
|
// 'apstreams' 계정이 있는지 확인
|
||||||
if (!in_array(ACTIVITYPUB_G5_USERNAME, $member_list['id'])) return;
|
if (!in_array(ACTIVITYPUB_G5_USERNAME, $member_list['id'])) return;
|
||||||
|
|
||||||
// 현재 로그인되어 있으면, 로그인된 계정의 정보를 따름
|
// 글을 생성한 회원 정보
|
||||||
$mb = (isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME));
|
$mb = (isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME));
|
||||||
|
|
||||||
// 글 전송하기
|
// 글 전송하기
|
||||||
if (!empty($mb['mb_id'])) {
|
if (!empty($mb['mb_id'])) {
|
||||||
// 글 전송하기
|
// 글 전송하기
|
||||||
$data = activitypub_publish_content(
|
activitypub_publish_content(
|
||||||
$me_memo,
|
$me_memo,
|
||||||
activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
|
activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
|
||||||
$mb
|
$mb
|
||||||
|
@ -1228,12 +1529,12 @@ function _activitypub_memo_form_update_after($member_list, $str_nick_list, $redi
|
||||||
function _activitypub_write_update_after($board, $wr_id, $w, $qstr, $redirect_url) {
|
function _activitypub_write_update_after($board, $wr_id, $w, $qstr, $redirect_url) {
|
||||||
global $g5, $member;
|
global $g5, $member;
|
||||||
|
|
||||||
// 본문 가져오기
|
// 본문 가져오기 (본문이 없는 경우 중단)
|
||||||
$sql = "select wr_id, wr_content from {$g5['write_prefix']}{$board['bo_table']} where wr_id = '{$wr_id}'";
|
$sql = "select wr_id, wr_content from {$g5['write_prefix']}{$board['bo_table']} where wr_id = '{$wr_id}'";
|
||||||
$row = sql_fetch($sql);
|
$row = sql_fetch($sql);
|
||||||
if (empty($row['wr_id'])) return;
|
if (empty($row['wr_id'])) return;
|
||||||
|
|
||||||
// 현재 로그인되어 있으면, 로그인된 계정의 정보를 따름
|
// 글을 생성한 회원 정보
|
||||||
$mb = (isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME));
|
$mb = (isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME));
|
||||||
|
|
||||||
// 추가할 오브젝트 속성
|
// 추가할 오브젝트 속성
|
||||||
|
@ -1247,7 +1548,7 @@ function _activitypub_write_update_after($board, $wr_id, $w, $qstr, $redirect_ur
|
||||||
|
|
||||||
// 글 전송하기
|
// 글 전송하기
|
||||||
if (!empty($mb['mb_id'])) {
|
if (!empty($mb['mb_id'])) {
|
||||||
$data = activitypub_publish_content(
|
activitypub_publish_content(
|
||||||
$row['wr_content'],
|
$row['wr_content'],
|
||||||
G5_BBS_URL . "/board.php?bo_table={$board['bo_table']}&wr_id={$row['wr_id']}",
|
G5_BBS_URL . "/board.php?bo_table={$board['bo_table']}&wr_id={$row['wr_id']}",
|
||||||
$mb,
|
$mb,
|
||||||
|
@ -1259,12 +1560,12 @@ function _activitypub_write_update_after($board, $wr_id, $w, $qstr, $redirect_ur
|
||||||
function _activitypub_comment_update_after($board, $wr_id, $w, $qstr, $redirect_url, $comment_id, $reply_array) {
|
function _activitypub_comment_update_after($board, $wr_id, $w, $qstr, $redirect_url, $comment_id, $reply_array) {
|
||||||
global $g5, $member;
|
global $g5, $member;
|
||||||
|
|
||||||
// 본문(댓글) 가져오기
|
// 본문(댓글) 가져오기 (본문이 없는 경우 중단)
|
||||||
$sql = "select wr_id, wr_content from {$g5['write_prefix']}{$board['bo_table']} where wr_id = '{$wr_id}'";
|
$sql = "select wr_id, wr_content from {$g5['write_prefix']}{$board['bo_table']} where wr_id = '{$wr_id}'";
|
||||||
$row = sql_fetch($sql);
|
$row = sql_fetch($sql);
|
||||||
if (empty($row['wr_id'])) return;
|
if (empty($row['wr_id'])) return;
|
||||||
|
|
||||||
// 현재 로그인되어 있으면, 로그인된 계정의 정보를 따름
|
// 글을 생성한 회원 정보
|
||||||
$mb = (isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME));
|
$mb = (isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME));
|
||||||
|
|
||||||
// 추가할 오브젝트 속성
|
// 추가할 오브젝트 속성
|
||||||
|
@ -1280,7 +1581,7 @@ function _activitypub_comment_update_after($board, $wr_id, $w, $qstr, $redirect_
|
||||||
|
|
||||||
// 글 전송하기
|
// 글 전송하기
|
||||||
if (!empty($mb['mb_id'])) {
|
if (!empty($mb['mb_id'])) {
|
||||||
$data = activitypub_publish_content(
|
activitypub_publish_content(
|
||||||
$row['wr_content'],
|
$row['wr_content'],
|
||||||
G5_BBS_URL . "/board.php?bo_table={$board['bo_table']}&wr_id={$row['wr_parent']}&c_id=" . $comment_id,
|
G5_BBS_URL . "/board.php?bo_table={$board['bo_table']}&wr_id={$row['wr_parent']}&c_id=" . $comment_id,
|
||||||
$mb,
|
$mb,
|
||||||
|
@ -1301,20 +1602,18 @@ while ($entry = $tmp->read()) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 모든 준비가 완료되고 작업 시작
|
// 모든 준비가 완료되고 작업 시작
|
||||||
$route = $_GET['route'];
|
$route = array_key_exists("route", $_GET) ? $_GET['route'] : "";
|
||||||
|
|
||||||
switch ($route) {
|
switch ($route) {
|
||||||
// 액펍(ActivityPub)과 웹핑거(WebFinger)는 다른 개념이지만, 여기서는 액펍(ActivityPub) 전용으로 사용한다.
|
// 일부 소프트웨어는 통신을 하기 위해 아래와 같이 설정을 해야할 수 있습니다. (선택사항)
|
||||||
// 액펍(ActivityPub)에서 사용자를 조회하기 전단계에서 이뤄지는 요청이다.
|
// .htaccess에 추가
|
||||||
//
|
|
||||||
// .htaccess에 추가 (추가해야만 그누보드 외 다른 플랫폼과 통신 가능, 그누보드 사이에서만 연결할 경우 필수사항 아님)
|
|
||||||
//
|
//
|
||||||
// <IfModule mod_rewrite.c>
|
// <IfModule mod_rewrite.c>
|
||||||
// RewriteEngine on
|
// RewriteEngine on
|
||||||
// RewriteRule ^\.well-known/webfinger /?route=webfinger [QSA,L]
|
// RewriteRule ^\.well-known/webfinger /?route=webfinger [QSA,L]
|
||||||
// </IfModule>
|
// </IfModule>
|
||||||
//
|
//
|
||||||
// Reference: https://wordpress.org/support/topic/htaccess-conflict/
|
// 참고: https://wordpress.org/support/topic/htaccess-conflict/
|
||||||
//
|
//
|
||||||
case "webfinger":
|
case "webfinger":
|
||||||
_GNUBOARD_ActivityPub::open();
|
_GNUBOARD_ActivityPub::open();
|
||||||
|
@ -1328,6 +1627,12 @@ switch ($route) {
|
||||||
_GNUBOARD_ActivityPub::close();
|
_GNUBOARD_ActivityPub::close();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "activitypub.activity":
|
||||||
|
_GNUBOARD_ActivityPub::open();
|
||||||
|
echo _GNUBOARD_ActivityPub::activity();
|
||||||
|
_GNUBOARD_ActivityPub::close();
|
||||||
|
break;
|
||||||
|
|
||||||
case "activitypub.inbox":
|
case "activitypub.inbox":
|
||||||
_GNUBOARD_ActivityPub::open();
|
_GNUBOARD_ActivityPub::open();
|
||||||
echo _GNUBOARD_ActivityPub::inbox();
|
echo _GNUBOARD_ActivityPub::inbox();
|
||||||
|
@ -1357,4 +1662,17 @@ switch ($route) {
|
||||||
echo _GNUBOARD_ActivityPub::liked();
|
echo _GNUBOARD_ActivityPub::liked();
|
||||||
_GNUBOARD_ActivityPub::close();
|
_GNUBOARD_ActivityPub::close();
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case "activitypub.shares":
|
||||||
|
_GNUBOARD_ActivityPub::open();
|
||||||
|
echo _GNUBOARD_ActivityPub::shares();
|
||||||
|
_GNUBOARD_ActivityPub::close();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "oauth2.authorize": // Not implemented
|
||||||
|
_GNUBOARD_ActivityPub::open();
|
||||||
|
echo _GNUBOARD_ActivityPub::authorize();
|
||||||
|
_GNUBOARD_ActivityPub::close();
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
112
lib/chatgpt.activitypub.lib.php
Normal file
112
lib/chatgpt.activitypub.lib.php
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
// GhatGPT-ActivityPub implementation for GNUBOARD 5
|
||||||
|
// Go Namhyeon <abuse@catswords.net>
|
||||||
|
// MIT License
|
||||||
|
// 2023-02-16
|
||||||
|
|
||||||
|
if (!defined('_GNUBOARD_') || !defined("ACTIVITYPUB_INSTANCE_ID")) exit; // 개별 페이지 접근 불가
|
||||||
|
|
||||||
|
// ChatGPT API 키 발급: https://platform.openai.com/account/api-keys
|
||||||
|
define("CHATGPT_API_KEY", "YOUR_API_KEY"); // API 키 입력
|
||||||
|
define("CHATGPT_API_URL", "https://api.openai.com/v1/completions"); // GhatGPT API 주소 입력
|
||||||
|
define("LINGVA_API_URL", "https://lingva.ml/api/v1"); // Lingva Translate (구글 번역기 프론트엔드) API 주소 입력
|
||||||
|
|
||||||
|
function lingva_translate($content, $source = 'ko', $target = 'en') {
|
||||||
|
$handle = curl_init();
|
||||||
|
curl_setopt($handle, CURLOPT_URL, LINGVA_API_URL . '/' . $source . '/' . $target . '/' . urlencode($content));
|
||||||
|
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
|
||||||
|
curl_setopt($handle, CURLOPT_SSL_VERIFYPEER, false);
|
||||||
|
$response = json_decode(curl_exec($handle), true);
|
||||||
|
curl_close($handle);
|
||||||
|
return $response['translation'];
|
||||||
|
}
|
||||||
|
|
||||||
|
function lingva_ko2en($content) {
|
||||||
|
return lingva_translate($content, 'ko', 'en');
|
||||||
|
}
|
||||||
|
|
||||||
|
function lingva_en2ko($content) {
|
||||||
|
return lingva_translate($content, 'en', 'ko');
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatgpt_request($content, $mb) {
|
||||||
|
// "What is the capital of France?"
|
||||||
|
$prompt = lingva_ko2en(filter_var($content, FILTER_SANITIZE_STRING));
|
||||||
|
$prompt = filter_var($content, FILTER_SANITIZE_STRING);
|
||||||
|
|
||||||
|
$data = array(
|
||||||
|
"model" => "text-davinci-003",
|
||||||
|
"prompt" => $prompt,
|
||||||
|
"max_tokens" => 3000,
|
||||||
|
"temperature" => 0.5,
|
||||||
|
);
|
||||||
|
|
||||||
|
$data_string = json_encode($data);
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt($ch, CURLOPT_URL, CHATGPT_API_URL);
|
||||||
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POST, true);
|
||||||
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $data_string);
|
||||||
|
curl_setopt($ch, CURLOPT_HTTPHEADER, array(
|
||||||
|
"Content-Type: application/json",
|
||||||
|
"Authorization: Bearer " . CHATGPT_API_KEY,
|
||||||
|
"Content-Length: " . strlen($data_string))
|
||||||
|
);
|
||||||
|
|
||||||
|
$output = curl_exec($ch);
|
||||||
|
curl_close($ch);
|
||||||
|
// print_r($output);
|
||||||
|
|
||||||
|
$output_json = json_decode($output, true);
|
||||||
|
$response = $output_json["choices"][0]["text"];
|
||||||
|
// echo $response;
|
||||||
|
|
||||||
|
return lingva_en2ko($response);
|
||||||
|
}
|
||||||
|
|
||||||
|
function chatgpt_send_conversation($content) {
|
||||||
|
global $member;
|
||||||
|
|
||||||
|
// 로그인 상태인 경우 해당 회원, 아닌 경우 ActivityPub 공통 계정
|
||||||
|
$mb = isset($member['mb_id']) ? $member : get_member(ACTIVITYPUB_G5_USERNAME);
|
||||||
|
|
||||||
|
// 수신자 목록
|
||||||
|
$to = array();
|
||||||
|
|
||||||
|
// 참고: 아래에 기술된 역할은 외부 프로그램이 담당하게 할 수도 있음 (service:chatgpt)
|
||||||
|
if (!empty(CHATGPT_API_KEY)) {
|
||||||
|
$response = chatgpt_request($content, $mb);
|
||||||
|
$to[] = "service:chatgpt";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Activity 발행 (발신: 그누보드5 -> ChatGPT)
|
||||||
|
activitypub_publish_content(
|
||||||
|
$content,
|
||||||
|
activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
|
||||||
|
get_member(ACTIVITYPUB_G5_USERNAME),
|
||||||
|
array(),
|
||||||
|
$to
|
||||||
|
);
|
||||||
|
|
||||||
|
// Activity 발행 (수신: ChatGPT -> 그누보드5)
|
||||||
|
activitypub_publish_content(
|
||||||
|
$response,
|
||||||
|
"service:chatgpt",
|
||||||
|
get_member(ACTIVITYPUB_G5_USERNAME),
|
||||||
|
array(),
|
||||||
|
array(
|
||||||
|
activitypub_get_url("user", array("mb_id" => $mb['mb_id']))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function _chatgpt_memo_form_update_after($member_list, $str_nick_list, $redirect_url, $me_memo) {
|
||||||
|
// 수신자에 'apstreams' 계정이 있는지 확인
|
||||||
|
if (!in_array(ACTIVITYPUB_G5_USERNAME, $member_list['id'])) return;
|
||||||
|
|
||||||
|
// ChatGPT에게 대화 걸기
|
||||||
|
chatgpt_send_conversation($me_memo);
|
||||||
|
}
|
||||||
|
|
||||||
|
add_event("memo_form_update_after", "_chatgpt_memo_form_update_after", 1, 4);
|
|
@ -2,7 +2,7 @@
|
||||||
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
|
||||||
|
|
||||||
// ActivityPub/WebHook implementation for GNUBOARD 5
|
// ActivityPub/WebHook implementation for GNUBOARD 5
|
||||||
// Go Namhyeon <gnh1201@gmail.com>
|
// Go Namhyeon <abuse@catswords.net>
|
||||||
// MIT License
|
// MIT License
|
||||||
// 2022-07-07
|
// 2022-07-07
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue
Block a user