Compare commits

...

45 Commits
0.1.14 ... main

Author SHA1 Message Date
c74fc25a2f
Update README.md 2024-02-16 15:38:06 +09:00
a2773e2705
Update README.md 2023-08-16 20:53:08 +09:00
f1e37c9ff5
Update README.md 2023-08-16 20:50:35 +09:00
56bf8b0293
Update README.md 2023-08-16 18:57:11 +09:00
e2f616df73
Update README.md 2023-08-16 18:46:24 +09:00
2f94ed4305
Update README.md 2023-08-16 18:19:22 +09:00
21abc54ccd
Update README.md 2023-08-16 18:18:37 +09:00
5b0f184ea6
Update activitypub.extend.php 2023-08-08 20:41:02 +09:00
8f5aed806c
Update README.md 2023-08-08 20:40:33 +09:00
641a9cd40e
Update activitypub.extend.php 2023-08-08 20:39:37 +09:00
c510019165
Update activitypub.extend.php 2023-08-08 19:53:48 +09:00
6bd734d50b
Update activitypub.extend.php 2023-08-08 18:25:03 +09:00
b201e9d789
Update activitypub.extend.php 2023-08-08 15:36:17 +09:00
99af00d0d7
Update activitypub.extend.php 2023-08-08 14:59:04 +09:00
ffe384916d
Update activitypub.extend.php 2023-08-08 14:57:47 +09:00
91c355ccde
Update activitypub.extend.php 2023-08-08 14:57:06 +09:00
131329c95a
Update activitypub.extend.php 2023-08-06 03:02:54 +09:00
bdb295b207
Update README.md 2023-07-29 22:47:32 +09:00
c3b7c9824d
Update README.md 2023-07-29 22:47:21 +09:00
2e8a090f53
Update activitypub.extend.php 2023-07-26 18:26:09 +09:00
0337c40a05
Update activitypub.extend.php 2023-07-26 18:22:31 +09:00
b3b8527746
Update README.md 2023-07-11 15:44:05 +09:00
edbfc8dd62
Update activitypub.extend.php 2023-07-11 15:37:06 +09:00
51c51ea9ed
Update README.md 2023-07-11 15:28:54 +09:00
af010ddd55
Update activitypub.extend.php 2023-07-11 15:27:28 +09:00
21b35d489b
Update activitypub.extend.php 2023-07-11 15:25:00 +09:00
76022bc3e1
Update activitypub.extend.php 2023-07-11 14:38:49 +09:00
59a1248bf6
Update activitypub.extend.php 2023-07-11 14:35:47 +09:00
ec972bc5a1
Update activitypub.extend.php 2023-07-11 14:11:07 +09:00
4190a21ba2
Update activitypub.extend.php 2023-04-18 17:25:23 +09:00
835200e5e6
Update activitypub.extend.php 2023-03-03 02:13:52 +09:00
7565b29a37
Update activitypub.extend.php 2023-02-16 19:38:37 +09:00
b43467459b
Update activitypub.extend.php 2023-02-16 19:18:22 +09:00
3823ba32b0
Update activitypub.extend.php 2023-02-16 19:18:07 +09:00
142b91685b
Update activitypub.extend.php 2023-02-16 17:13:30 +09:00
7691f6f820
Update README.md 2023-02-16 17:03:42 +09:00
41d93d08ec
Rename chatgpt.activitypub.extend.php to chatgpt.activitypub.lib.php 2023-02-16 16:46:22 +09:00
4e7b20e593
Create chatgpt.activitypub.extend.php 2023-02-16 16:46:07 +09:00
ac50961dde
Merge pull request #8 from gnh1201/add-genrsa
Update activitypub.extend.php
2023-02-16 15:04:08 +09:00
a1b851fd3d
Update activitypub.extend.php 2023-02-16 15:03:43 +09:00
ea3b0e1e86
Merge pull request #7 from gnh1201/add-genrsa
Add genrsa
2023-02-16 14:24:41 +09:00
45244d7188
Update activitypub.extend.php 2023-02-16 14:20:21 +09:00
a9da7f91c3
Update activitypub.extend.php 2023-02-16 13:53:24 +09:00
d3fcaf352d
Update activitypub.extend.php 2023-02-16 13:50:29 +09:00
25ff6427ff
Update activitypub.extend.php 2023-02-16 11:54:38 +09:00
3 changed files with 446 additions and 218 deletions

View File

@ -1,11 +1,14 @@
# gnuboard5-activitypub # gnuboard5-activitypub
ActivityPub implementation for GNUBOARD 5 GNUBOARD5-ActivityPub: ActivityPub (Fediverse) implementation for GNUBOARD5
* https://sir.kr/g5_plugin/10381
* https://codeberg.org/fediverse/delightful-activitypub-development
## 사용 전 설정 ## 사용 전 설정
* `apstreams` 게시판 추가 * `apstreams` 게시판 추가
* `apstreams` 사용자 추가 * `apstreams` 사용자 추가
## 작업진행 ## 지원현황
- [x] WebFinger - [x] WebFinger
- [x] User - [x] User
- [x] Inbox - [x] Inbox
@ -13,22 +16,24 @@ ActivityPub implementation for GNUBOARD 5
- [x] Followers - [x] Followers
- [x] Following - [x] Following
- [x] Liked - [x] Liked
- [x] Shares - [ ] ~~Shares~~ (Altered to inbound/outbound)
- [x] Geolocation (IP2Location, Naver Cloud) - [x] Geolocation
- [x] File attachment - [x] File attachment
- [ ] File attachment -> Auto sharing (Download/Upload) - [ ] 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] 아바타 (gravatar.com)
- [x] 날씨 (openweathermap.org) - [x] 날씨 (openweathermap.org)
- [x] 환율 (koreaexim.go.kr) - [x] 환율 (koreaexim.go.kr)
- [x] 국내 Geolocation (Naver Cloud)
- [x] 국외 Geolocation (IP2Location)
## 향후 지원 예정 ## 전문(메시지) 예시
- [ ] OAuth 2.0
- [ ] w3id.org (`publicKey` 필드) 지원
- [ ] 메시지 큐(Redis, RebbitMQ, Kafka 등) 지원
## 전문 예시
```json ```json
{ {
@ -116,6 +121,11 @@ ActivityPub implementation for GNUBOARD 5
* 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://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
## 문의 ## 문의
* abuse@catswords.net * abuse@catswords.net
* ActivityPub [@gnh1201@catswords.social](https://catswords.social/@gnh1201)

View File

@ -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 <abuse@catswords.net> // Author: Go Namhyeon (Catswords Research) <abuse@catswords.net>
// MIT License // ActivityPub: @gnh1201@catswords.social
// 2023-02-16 (version 0.1.14) // 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/
@ -17,17 +18,24 @@ if (!defined('_GNUBOARD_')) exit; // 개별 페이지 접근 불가
// * 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://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.14-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_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_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("ACTIVITYPUB_CERTIFICATE_DATAFIELD", "mb_9"); // 회원별 인증서(공개키, 개인키)를 저장할 필드 (기본: mb_9)
define("OAUTH2_GRANT_DATAFIELD", "mb_10"); // 회원별 인증 정보를 저장할 필드 (기본: mb_10) define("OAUTH2_GRANT_DATAFIELD", "mb_10"); // 회원별 인증 정보를 저장할 필드 (기본: mb_10)
define("DEFAULT_HTML_ENTITY_FLAGS", ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401); define("DEFAULT_HTML_ENTITY_FLAGS", ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401);
@ -66,6 +74,17 @@ 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() { function activitypub_create_keypair() {
$keypair = array('', ''); $keypair = array('', '');
@ -96,36 +115,43 @@ function activitypub_get_stored_keypair($mb) {
$private_key = ''; $private_key = '';
$public_key = ''; $public_key = '';
// 인증서 정보 불러오기 // 인증서 생성
if ($mb != null && !empty($mb['mb_id'])) { $k = 0;
$certificate_data = activitypub_parse_stored_data($mb[ACTIVITYPUB_CERTIFICATE_DATAFIELD]); while ($k < ACTIVITYPUB_CERTIFICATE_RETRY && (empty($private_key) || empty($public_key))) {
$private_key = activitypub_get_memo($certificate_data['PrivateKeyId']); // 개인키(Private Key) // 인증서 정보 불러오기
$public_key = activitypub_get_memo($certificate_data['PublicKeyId']); // 공개키(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);
} }
// 인증서 정보 불러오기 // 인증서 정보가 없으면 생성
$certificate_data = activitypub_parse_stored_data($mb[ACTIVITYPUB_CERTIFICATE_DATAFIELD]); if (!$mb[ACTIVITYPUB_CERTIFICATE_DATAFIELD] || empty($private_key) || empty($public_key)) {
$private_key = activitypub_get_memo($certificate_data['PrivateKeyId']); // 개인키(Private Key) $keypair = activitypub_create_keypair(); // 인증서(공개키, 개인키) 생성
$public_key = activitypub_get_memo($certificate_data['PublicKeyId']); // 공개키(Public Key) $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)
// 회원에게 알림 // 회원 정보에 등록
activitypub_add_memo(ACTIVITYPUB_G5_USERNAME, $mb['mb_id'], "외부 서버와 통신하기 위한 인증서가 발급되었습니다. 발급된 인증서는 두가지(개인키, 공개키)입니다. 인증서를 삭제하거나 타인과 공유하지 마세요. A certificate has been issued to communicate with an external server. There are two certificates issued (private key, public key). Do not delete the certificate or share it with others."); 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); return array($private_key, $public_key);
@ -150,8 +176,8 @@ 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_parse_stored_data($s) { function activitypub_parse_stored_data($s) {
@ -204,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() {
@ -318,14 +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 = '') {
// make HTTP header // build the header
$headers = array("Accept" => "application/ld+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\""); $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;
} }
// do HTTP request // request
$ch = curl_init(); $ch = curl_init();
curl_setopt_array($ch, array( curl_setopt_array($ch, array(
CURLOPT_URL => $url, CURLOPT_URL => $url,
@ -358,17 +445,28 @@ function activitypub_get_attachments($bo_table, $wr_id) {
return $attachments; return $attachments;
} }
function activitypub_http_post($url, $raw_data, $access_token = '', $mb = null) { function activitypub_http_post($url, $rawdata, $mb, $access_token = '') {
// make HTTP header // get digest
$headers = array("Accept" => "application/ld+json; profile=\"" . NAMESPACE_ACTIVITYSTREAMS . "\""); $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;
} }
list($private_key, $public_key) = activitypub_get_stored_keypair($mb);
// TODO: make Signature header // request
// do HTTP request
$ch = curl_init(); $ch = curl_init();
curl_setopt_array($ch, array( curl_setopt_array($ch, array(
CURLOPT_URL => $url, CURLOPT_URL => $url,
@ -376,12 +474,18 @@ function activitypub_http_post($url, $raw_data, $access_token = '', $mb = null)
CURLOPT_SSL_VERIFYPEER => false, CURLOPT_SSL_VERIFYPEER => false,
CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_RETURNTRANSFER => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_POSTFIELDS => $raw_data, CURLOPT_POSTFIELDS => $rawdata,
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);
} }
@ -477,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) {
@ -564,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']) {
@ -573,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)에 반영
} }
} }
} }
@ -603,48 +735,57 @@ function activitypub_publish_content($content, $object_id, $mb, $_added_object =
// 위치정보가 활성화되어 있으면 // 위치정보가 활성화되어 있으면
if (ACTIVITYPUB_ENABLED_GEOLOCATION) { if (ACTIVITYPUB_ENABLED_GEOLOCATION) {
$object = array_merge($_added_object, array( $_added_object = array_merge($_added_object, array(
"location" => $location_ctx "location" => $location_ctx
)); ));
} }
// 태그 추가
$_added_object = array_merge($_added_object, array(
"tag" => $tag
));
// 전문 생성 // 전문 생성
$object = activitypub_build_note($content, $object_id, $mb, $_added_object); $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;
} }
@ -659,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)으로 상태 업데이트
@ -674,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;
} }
@ -736,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'];
@ -809,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);
@ -852,50 +994,37 @@ 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;
return activitypub_build_collection($items);
} }
// Object type: Note // Object type: Note
function activitypub_build_note($content, $object_id, $mb, $_added_object = array()) { function activitypub_build_note($content, $object_id, $mb, $_added_object = array()) {
return array_merge(array( return array_merge(array(
"type" => "Note", "type" => "Note",
"generator" => "GNUBOARD5-ActivityPub/" . ACTIVITYPUB_INSTANCE_VERSION . " (" . ACTIVITYPUB_INSTANCE_ID . ")", "generator" => "G5.ActivityPub/" . ACTIVITYPUB_INSTANCE_VERSION . " (GNUBOARD " . G5_GNUBOARD_VER . "; " . ACTIVITYPUB_INSTANCE_ID . ")",
"id" => $object_id, "id" => $object_id,
"attributedTo" => activitypub_get_url("user", array("mb_id" => $mb['mb_id'])), "attributedTo" => activitypub_get_url("user", array("mb_id" => $mb['mb_id'])),
"content" => $content, "content" => $content,
@ -907,7 +1036,7 @@ function activitypub_build_note($content, $object_id, $mb, $_added_object = arra
function activitypub_build_collection($items, $summary = '') { function activitypub_build_collection($items, $summary = '') {
return array( return array(
"@context" => NAMESPACE_ACTIVITYSTREAMS, "@context" => NAMESPACE_ACTIVITYSTREAMS,
"generator" => "GNUBOARD5-ActivityPub/" . ACTIVITYPUB_INSTANCE_VERSION . " (" . ACTIVITYPUB_INSTANCE_ID . ")", "generator" => "G5.ActivityPub/" . ACTIVITYPUB_INSTANCE_VERSION . " (GNUBOARD " . G5_GNUBOARD_VER . "; " . ACTIVITYPUB_INSTANCE_ID . ")",
"summary" => $summary, "summary" => $summary,
"type" => "Collection", "type" => "Collection",
"totalItems" => count($items), "totalItems" => count($items),
@ -918,7 +1047,7 @@ function activitypub_build_collection($items, $summary = '') {
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() {
@ -1014,17 +1143,15 @@ class _GNUBOARD_ActivityPub {
"@context" => array(NAMESPACE_ACTIVITYSTREAMS, NAMESPACE_W3ID_SECURITY_V1, array("@language" => "ko")), "@context" => array(NAMESPACE_ACTIVITYSTREAMS, NAMESPACE_W3ID_SECURITY_V1, array("@language" => "ko")),
"type" => "Person", "type" => "Person",
"id" => $activitypub_user_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")
), ),
@ -1032,12 +1159,25 @@ class _GNUBOARD_ActivityPub {
"id" => $activitypub_user_id . "#main-key", "id" => $activitypub_user_id . "#main-key",
"owner" => $activitypub_user_id, "owner" => $activitypub_user_id,
"publicKeyPem" => $public_key "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']) {
@ -1046,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"));
} }
@ -1062,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'];
@ -1084,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'])) {
@ -1273,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"));
@ -1293,7 +1440,7 @@ 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"));
} }
} }
@ -1314,66 +1461,20 @@ class _GNUBOARD_ActivityPub {
public static function shares() { public static function shares() {
global $g5; global $g5;
// 게시판인 경우 $items = array(); // 항목을 담을 배열
if (array_key_exists("bo_table", $_GET)) {
$bo = get_board_db($_GET['bo_table'], true);
if (!empty($bo['bo_table'])) { // 최근 활동에서 추출
switch($bo['bo_table']) { $sql = "select * from " . $g5['board_new_table'];
case ACTIVITYPUB_G5_BOARDNAME: $result = sql_query($sql);
return self::inbox(); // 액티비티를 저장하는 테이블인 경우 inbox와 동일하게 취급 while ($row = sql_fetch_array($result)) {
$write_table = $g5['write_prefix'] . $row['bo_table'];
default: $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 ";
$items = array(); // 항목을 담을 배열 $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']}";
$page = intval($_GET['page']); $mb = get_member($row2['mb_id']);
if ($page < 1) { $content = $row2['wr_content'];
$page = 1; array_push($items, activitypub_build_note($content, $object_id, $mb));
}
// 페이지 당 표시할 게시물 수 불러오기
$page_rows = 0;
if (!empty($bo['bo_mobile_page_rows'])) {
$page_rows = intval($bo['bo_mobile_page_rows']);
} else if (!empty($bo['bo_page_rows'])) {
$page_rows = intval($bo['bo_page_rows']);
}
// 페이지 당 표시할 게시물 수가 1보다 작으면 기본값(15)로 설정
if ($pages_rows < 1) {
$page_rows = 15;
}
// SQL 작성
$write_table = $g5['write_prefix'] . $bo['bo_table'];
$offset = ($page - 1) * $page_rows;
$sql = "select wr_id, mb_id, wr_content, wr_datetime from {$write_table} where FIND_IN_SET('secret', wr_option) = 0 order by wr_datetime desc limit {$offset}, {$page_rows} ";
// SQL 실행
$result = sql_query($sql);
while ($row = sql_fetch_array($result)) {
$object_id = G5_BBS_URL . "/board.php?bo_table={$bo['bo_table']}&wr_id={$row['wr_id']}";
$mb = get_member($row['mb_id']);
$content = $row['wr_content'];
array_push($items, activitypub_build_note($content, $object_id, $mb));
}
}
}
} else { // 게시판이 아닌 경우
// 최근 활동에서 추출
$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));
}
} }
} }
@ -1386,15 +1487,15 @@ class _GNUBOARD_ActivityPub {
switch ($grant_type) { switch ($grant_type) {
case "authorization_code": case "authorization_code":
return activitypub_json_encode(array("message" => "Sorry. This grant type does not supported yet")); return activitypub_json_encode(array("message" => "Not implemented"));
break; break;
case "password": case "password":
return activitypub_json_encode(array("message" => "Sorry. This grant type does not supported yet")); return activitypub_json_encode(array("message" => "Not implemented"));
break; break;
case "client_credentials": case "client_credentials":
return activitypub_json_encode(array("message" => "Sorry. This grant type does not supported yet")); return activitypub_json_encode(array("message" => "Not implemented"));
break; break;
} }
} }
@ -1411,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
@ -1428,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));
// 추가할 오브젝트 속성 // 추가할 오브젝트 속성
@ -1447,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,
@ -1459,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));
// 추가할 오브젝트 속성 // 추가할 오브젝트 속성
@ -1480,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,
@ -1504,17 +1605,15 @@ while ($entry = $tmp->read()) {
$route = array_key_exists("route", $_GET) ? $_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();
@ -1528,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();
@ -1564,9 +1669,10 @@ switch ($route) {
_GNUBOARD_ActivityPub::close(); _GNUBOARD_ActivityPub::close();
break; break;
case "oauth2.authorize": // TODO case "oauth2.authorize": // Not implemented
_GNUBOARD_ActivityPub::open(); _GNUBOARD_ActivityPub::open();
echo _GNUBOARD_ActivityPub::authorize(); echo _GNUBOARD_ActivityPub::authorize();
_GNUBOARD_ActivityPub::close(); _GNUBOARD_ActivityPub::close();
break; break;
} }

View 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);