Pipe Modülü Yazma

1.7k görüntülenme Markdown

Pipe modülü bir destek posta kutusunu okur ve normalleştirilmiş bir mesaj dizisi döndürür; cron zinciri onu talebe ve yanıta çevirir.

Genel Bakış

Pipe, destek talebi e-posta alım tipidir. Üç modül gelir: Google, Microsoft ve Pop3. Taban sınıf yoktur.

Yapılandırma birimi modül değil departmandır; bağlantı durumuna dokunan her metot bir departman kimliği alır.

İstek döngüsünde modülünüze yalnız ayar ekranı ve OAuth geri çağrısı dokunur. Okumayı cron yapar: keşif getirme işini başlatır, getirme inbox() çağırır, üçüncü iş talebi oluşturur.

Ön Koşullar

  • coremio/modules/Pipe/ altına yazma erişimi.
  • Erişilebilir bir posta kutusu ve imap eklentisi ya da OAuth uygulaması.
  • Önceden oluşturulmuş destek talebi departmanları.
  • Zamanlanmış Görev Ekleme.

Yapı

Pipe sınıfları ad alanlıdır: WISECP\Modules\Pipe. Cron bu adı sağlayıcı dizesinden kurar.

düzen
coremio/modules/Pipe/Acme/
├── Acme.php                    namespace WISECP\Modules\Pipe; class Acme
├── config.php                  genel sağlayıcı bloğu + departman id başına bir blok
├── lang/en.php                 alan etiketleri, description, setup-guide
├── lang/tr.php
├── logo.svg
└── views/
    ├── credentialsForm.php     departman başına alanlar, ayarlar sekmesine render edilir
    └── providerForm.php        yalnız OAuth uygulamaları: genel client id ve secret

OAuth yönlendiricisi paylaşılandır ve her parçayı bir modüle eşler.

Adım Adım

Modül Sınıfını Kurma

  1. coremio/modules/Pipe/Acme/Acme.php dosyasını namespace WISECP\Modules\Pipe; ve class Acme ile oluşturun.
  2. Sınıfa dizinle eşleşen public bir $name verin.
  3. save_config(array $data) özyinelemeli birleştirme olmalı.
  4. is_connected(int $did) uygulayın; false diyen departman atlanır.
  5. inbox(int $did) uygulayın.
  6. test_connection(int $did) uygulayın.

Departman Formu

  1. views/credentialsForm.php oluşturun; $mv, $mk ve $did alır.
  2. Alanları module[{modülAnahtarı}][{did}][{alan}] olarak adlandırın.
  3. Mevcut değerleri $mv["init"]->config[$did], etiketleri $mv["lang"] içinden okuyun.
  4. Uzun anlatım setup-guide anahtarına konur.

OAuth Ekleme

  1. is_configured() bildirin; varlığı sağlayıcı kartının anahtarıdır.
  2. get_global_config(), save_provider_config() ve views/providerForm.php ekleyin.
  3. Yönlendirme adresini paylaşılan rota anahtarıyla üretin: LinkGenerator::client('pipe-oauth-callback', [strtolower($this->name)]).
  4. Yetkilendirme adresini döndüren oAuth2(int $did) ve callback_handle() ekleyin.
  5. Parçanızı yönlendiriciye kaydedin.
  6. clear_tokens(int $did) ekleyin.

Referans

Metot Sözleşmesi

Her biri çağrılmadan önce method_exists ile yoklanır.

MetotÇağıranYoksa ne olur
inbox(int $did): arraygetirme cron'uiş inbox-missing ile düşer
is_connected(int $did): boolkeşif cron'udepartman yine başlatılır
save_config(array $data)ayar kaydetmealanlar saklanmaz
test_connection($did): arrayayar ekranıtest butonu çalışmaz
is_configured(): boolayar ekranısağlayıcı kartı çıkmaz
get_global_config(): arrayayar ekranıkart boş görünür
save_provider_config($id, $secret): boolsağlayıcı kaydetmekaydetme istisna fırlatır
oAuth2($did): arrayayar ekranıbağlan butonu çalışmaz
callback_handle(): arraypaylaşılan OAuth rotasıişleyici bulunamaz
clear_tokens($did)ayar ekranıbağlantı kesilemez
get_connected_email($did): stringkimlik bilgisi formuposta kutusu gösterilmez

Panelden yalnız oAuth2, test_connection ve clear_tokens çağrılabilir.

İmzalar

imzalar
public function __construct();

// Özyinelemeli birleştirme, sonra config.php yeniden yazılır. Düz bir atama diğer departmanları siler.
public function save_config($data = []);

// TEK bir departmanın hazırlığı. Token'lar var, ya da hostname+username+password dolu.
public function is_connected(int $did): bool;

// Hattın olmadan yapamayacağı tek metot. Dönüş biçimi aşağıda.
public function inbox(int $did): array;

// ['status' => 'successful'] ya da ['status' => 'error', 'message' => '...'].
public function test_connection($did = 0): array;

// Yalnız OAuth modülleri.
public function is_configured(): bool;
public function get_global_config(): array;                                    // client_id, client_secret, redirect_uri
public function save_provider_config(string $client_id, string $client_secret_plain): bool;
public function oAuth2($did = 0): array;                                       // ['status' => 'successful', 'redirect' => $url]
public function callback_handle(): array;                                      // ['status' => 'connected', 'did' => 4, 'email' => '...']
public function clear_tokens($did = 0);
public function get_connected_email(int $did): string;

Asıl sözleşme mesaj dizisidir; eksik anahtar sessizce eksilir.

inbox() dönüşü
return [
    'status' => 'successful',            // başka her şey yumuşak hata sayılır
    'data'   => [
        [
            'ip'          => '203.0.113.9',              // başlıklar taşıyorsa gönderen IP'si, yoksa ''
            'date'        => '2026-08-03 09:41:00',      // Y-m-d H:i:s, yinelenen özetinde kullanılır
            'subject'     => 'Cannot reach my panel',    // talebin konusu olur
            'spam'        => false,                      // true işleyicinin mesajı düşürmesini sağlar
            'msgid'       => '',                         // isteğe bağlı; boşken kararlı bir özet üretilir
            'from'        => ['name' => 'Ada L.', 'address' => '[email protected]'],
            'to'          => ['name' => 'Support', 'address' => '[email protected]'],
            'message'     => '<p>The panel times out.</p>',
            'attachments' => [
                [
                    'file_name' => 'screenshot.png',     // gönderenin kullandığı ad
                    'name'      => 'a1b2c3d4e5f6.png',   // rastgeleleştirilmiş saklanan ad
                    'file_ext'  => 'png',
                    'size'      => 20481,
                    'content'   => 'iVBORw0KGgoAAAANS',  // ham baytların base64'ü
                ],
            ],
        ],
    ],
];

// Hatada ya fırlatın ya da yumuşak hata biçimini döndürün:
return ['status' => 'error', 'message' => 'Cannot connect to server', 'data' => []];

Fırlatmak devre kesiciyi kurar ve işi başarısız kaydeder; dönen error durumu işi iptal işaretler. Yapılandırma için döndürün, taşıma için fırlatın.

Config Düzeni

config.php modül geneli için dize, departman durumu için tam sayı anahtar taşır. save_config() bu yüzden birleştirmelidir.

config.php
<?php
return [
    'lookback_days' => 3,                        // modül geneli
    'provider'      => [                         // modül geneli, yalnız OAuth uygulamaları
        'client_id'     => '...',
        'client_secret' => '...',                // crypt/system ile Crypt::encode
    ],
    4 => [                                       // departman 4
        'tokens' => '...',                       // token JSON'unun Crypt::encode'u
        'email'  => '[email protected]',
    ],
    7 => [                                       // departman 7, kimlik bilgisi tabanlı
        'protocol' => 'imap',
        'hostname' => 'mail.example.com',
        'port'     => 993,
        'username' => '[email protected]',
        'password' => '...',
        'ssl'      => true,
    ],
];

Posta kutusu ile departman eşlemesi paylaşılan seçenekler dosyasındadır:

options/ticket-pipe/status Ana anahtar; kapalıyken keşif pipe-disabled ile iptal döner.
options/ticket-pipe/mail Departman kimliğini provider, from, fname satırına eşler.
options/ticket-pipe/prefix Giden konulara yazılıp dönüşte eşleştirilen referans etiketi. Varsayılanı REF.
options/ticket-pipe/existing-client Gönderen bir hesapla eşleşmediğinde reddetmek mi, oluşturmak mı.
options/ticket-pipe/spam-control Açıkken spam denetimleri konu ve gönderen üzerinde önce çalışır.
Config::save() İç içedir: alt ağacın tamamını yazın, Config::save("options", Config::set("options", ['ticket-pipe' => $ticketPipe])).

Cron Zinciri

Hattı üç kuyruk işleyicisi kurar:

İşleyiciNe yaparModülünüze dokunduğu yer
discoverDepartmanları gezer, her biri için bir getirme işi başlatıris_connected() ve sınıf kontrolü
fetchMesajları toplar, her biri için bir mesaj işi başlatırinbox()
messageMesajı talebe ya da yanıta çevirirhiçbir şey; dizinizi okur

Dört koşul keşfin bir departmanı hatasız atlamasına yol açar: sınıf yok, is_connected() false, getirme havada, ya da bekleme penceresi. Pencere üç saattir.

Örnek

Kimlik bilgisi tabanlı bir modül, sonra onu çağıran cron kodu.

Acme.php
<?php
namespace WISECP\Modules\Pipe;

class Acme
{
    public $name = "Acme";
    public $config = [];
    public $lang = [];
    public $test = false;

    public function __construct()
    {
        $this->config = \Modules::Config("Pipe", $this->name);
        $this->lang   = \Modules::Lang("Pipe", $this->name);
    }

    // Özyinelemeli birleştirme: ayar ekranı her seferinde tek departman gönderir.
    public function save_config($data = [])
    {
        $merged       = array_replace_recursive($this->config ?: [], $data);
        $this->config = $merged;

        return \FileManager::file_write(__DIR__ . DS . "config.php", \Utility::array_export($merged, ['pwith' => true]));
    }

    public function is_connected(int $did): bool
    {
        $cfg = $this->config[$did] ?? null;
        if (!is_array($cfg)) return false;

        return !empty($cfg['hostname']) && !empty($cfg['username']) && !empty($cfg['password']);
    }

    public function test_connection($did = 0): array
    {
        try {
            // Form module[Acme][{did}][field] olarak gönderir, okunacak yol budur.
            $host = trim((string) \Filter::init("POST/module/" . $this->name . "/" . $did . "/hostname"));
            $user = trim((string) \Filter::init("POST/module/" . $this->name . "/" . $did . "/username"));
            $pass = (string) \Filter::init("POST/module/" . $this->name . "/" . $did . "/password", "password");

            if ($host === '' || $user === '' || $pass === '')
                throw new \Exception($this->lang["credentials-required"] ?? 'Hostname, username and password are required.');

            $this->open($host, $user, $pass);
        }
        catch (\Exception $e) {
            return ['status' => "error", 'message' => $e->getMessage()];
        }

        return ['status' => "successful"];
    }

    public function inbox(int $did): array
    {
        $cfg = $this->config[$did] ?? null;

        // Yapılandırma sorunu yumuşak hatadır: iş başarısız değil iptal edilir.
        if (!$cfg) return ['status' => 'error', 'message' => 'Department config not found', 'data' => []];

        // Taşıma sorunu fırlatır: kesici açılır ve iş başarısız olarak kaydedilir.
        $session = $this->open($cfg['hostname'] ?? '', $cfg['username'] ?? '', $cfg['password'] ?? '');

        $lookback = max(1, (int) ($this->config['lookback_days'] ?? 3));
        $since    = date('Y-m-d', strtotime('-' . $lookback . ' days'));

        $messages = [];
        foreach ($this->unread($session, $since) as $raw) {
            $messages[] = [
                'ip'          => (string) ($raw['sender_ip'] ?? ''),
                'date'        => \DateManager::format("Y-m-d H:i:s", $raw['date'] ?? ''),
                'subject'     => (string) ($raw['subject'] ?? ''),
                'spam'        => false,
                'from'        => ['name' => (string) ($raw['from_name'] ?? ''), 'address' => (string) ($raw['from'] ?? '')],
                'to'          => ['name' => (string) ($raw['to_name'] ?? ''),   'address' => (string) ($raw['to'] ?? '')],
                'message'     => (string) ($raw['html'] ?? ($raw['text'] ?? '')),
                'attachments' => $this->attachments($raw['parts'] ?? []),
            ];
        }

        return ['status' => "successful", 'data' => $messages];
    }
}
coremio/cronjobs/TicketPipeFetch.php
// new değil getInstance: önce Modules::Load koşar ve modül config önbelleğini doldurur.
// Çıplak bir new ile yapıcı Modules::Config içinden null okur ve inbox() metodu
// "Department config not found" diyerek çıkar.
$module = \Modules::getInstance("Pipe", $provider);

if (!$module) return ['success' => false, 'result' => ['reason' => 'module-missing']];
if (!method_exists($module, 'inbox')) return ['success' => false, 'result' => ['reason' => 'inbox-missing']];

try {
    $response = $module->inbox($did);
}
catch (\Throwable $e) {
    self::set_cooldown($did, $e->getMessage());
    Admin::notify('ticket-pipe-failure', self::failure_payload($did, $provider, $depName, $depFrom, $e->getMessage()), 'error', [
        'dedupe_keys' => ['did', 'provider'],
        'recurring'   => true,
    ]);
    throw $e;
}

$status = (string) ($response['status'] ?? '');
$data   = (array)  ($response['data']   ?? []);

if ($status !== 'successful') {
    self::set_cooldown($did, (string) ($response['message'] ?? 'Unknown module error'));
    // ... bildir, sonra iptal edilmiş bir iş döndür
}

// Temiz koşu: bekleyen hata bildirimini düşür ve kesiciyi serbest bırak.
Admin::notify_resolve('ticket-pipe-failure', ['did' => $did, 'provider' => $provider]);
self::clear_cooldown($did);

Modüller mesaj kimliği döndürmüyor; getirme işleyicisi departman, gönderen, tarih ve konudan özet üretir.

Tuzaklar

Düz bir anahtar cron'a ulaşmaz

Her şey options/ticket-pipe altındadır. Benzer adlı üst seviye anahtar şikâyetsiz kaydedilir ve geri okunur, cron ise eski değeri görür.

save_config birleştirmeli, üzerine yazmamalı

Ayar gönderimi yalnız düzenlenen departmanı taşır. Atama yapan bir save_config() diğer departmanların kimlik bilgilerini siler; tek belirti o kutuların yoklanmamasıdır.

Örneği fabrikayla kurun

Modules::getInstance() önce yükleyiciyi çalıştırıp config önbelleğini doldurur. Çıplak bir new config'i null bırakır; hata inbox() içinde çıkar.

Hata departmanı üç saat uykuya yatırır

Devre kesici, bozuk bir kutunun dakika başı bildirim çıkarmasını engeller. Test sırasında düzeltmeniz işe yaramamış görünür: departman cooldown ile atlanır. Getirme işini doğrudan yeniden başlatın.

Token'lar sistem anahtarından geçer

OAuth token'ları ve client secret'lar Crypt::encode($value, Config::get("crypt/system")) ile saklanır: kullanıcı anahtarı değil, sistem anahtarı. Token JSON'unu kodlanmış saklayın.

Faydalı oldu mu?

Geri bildiriminiz için teşekkürler!

Hâlâ Yardıma mı İhtiyacınız Var?

Yukarıda bulamadığınız her şey için destek ekibimiz her zaman yanınızda.