네이버 블로그 이웃,그룹 관리 어드밴스드

네이버 블로그의 이웃,그룹 관리 기능을 확장합니다.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @namespace    https://tampermonkey.myso.kr/
// @name         네이버 블로그 이웃,그룹 관리 어드밴스드
// @description  네이버 블로그의 이웃,그룹 관리 기능을 확장합니다.
// @copyright    2021, myso (https://tampermonkey.myso.kr)
// @license      Apache-2.0
// @version      1.0.15
// @author       Won Choi
// @connect      naver.com
// @match        *://admin.blog.naver.com/BuddyListManage*
// @grant        GM_addStyle
// @grant        GM_xmlhttpRequest
// @require      https://cdn.jsdelivr.net/npm/[email protected]/assets/vendor/gm-app.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/assets/vendor/gm-add-style.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/assets/vendor/gm-add-script.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/assets/vendor/gm-xmlhttp-request-async.js
// @require      https://cdn.jsdelivr.net/npm/[email protected]/assets/donation.js
// ==/UserScript==

// ==OpenUserJS==
// @author myso
// ==/OpenUserJS==
GM_App(async function main() {
  GM_donation('.admin_set_buddy');
  GM_addScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js');
  GM_addScript('https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js');
  GM_addScript(() => {
    async function delete_buddy(selfishes) {
      const blogId = new URL(location.href).searchParams.get('blogId') || location.pathname.split('/')[1];
      const uri = new URL('https://admin.blog.naver.com/BuddyDelete.naver');
      const formData = new FormData(); selfishes.map(o=>formData.append('buddyBlogNo', o.blogNo));
      formData.append('blogId', blogId);
      formData.append('on', ''); formData.append('force', 'true');
      await fetch(uri.toString(), { method: 'POST', body: formData });
    }
    window.delete_buddy = delete_buddy;
  })
  GM_addScript(() => {
    async function search_buddy_page(callback, page = 1, results = []) {
      const blogId = new URL(location.href).searchParams.get('blogId') || location.pathname.split('/')[1];
      const res = await fetch(`https://admin.blog.naver.com/BuddyListManage.naver?blogId=${blogId}&currentPage=${page}&searchText=&orderType=adddate`).then(r=>r.text());
      const doc = document.createElement('div'); doc.innerHTML = res;
      const pagination = Array.from(doc.querySelector('div.paginate_re').children), pagenation_last = pagination[pagination.length - 1];
      const has_next = pagenation_last.tagName == 'A' && !!pagenation_last.className, has_next_valid = pagenation_last.tagName != 'STRONG';
      const usernames_rows = Array.from(doc.querySelectorAll('tr a[href*="blog.naver.com"], tr a[href*=".blog.me"]'));
      const usernames = usernames_rows.map(e=>{
        const uri = new URL(e.href);
        const tr = e.closest('tr');
        const group = (() => {
          const el = tr.querySelector('td.groupwrap, td:nth-child(2)');
          return _.trim(el ? el.innerText : '');
        })();
        const type = (() => {
          const el = tr.querySelector('td.type, td:nth-child(3)');
          return _.trim(el ? el.innerText : '');
        })();
        const blogNo = (() => tr.querySelector('td:first-child input').value)();
        const blogId = (() => {
          if(uri.hostname.includes('.blog.me')) return uri.hostname.replace('.blog.me', '');
          if(uri.hostname.includes('blog.naver.com')) return uri.pathname.replace('/', '');
          return uri.hostname;
        })();
        const blogUrl = (() => e.href)();
        const blogName = (() => {
          const el = tr.querySelector('td.buddy a, td:nth-child(4) a');
          return _.trim(el ? el.innerText : blogId);
        })();
        const nickName = (() => {
          const el = tr.querySelector('td.buddy span.nickname, td:nth-child(4) span.nickname');
          return _.trim(el ? el.innerText : blogId);
        })();
        const lastPostAt = (() => {
          const el = tr.querySelector('td:nth-child(6)');
          const date = _.trim(el ? el.innerText : '');
          return /^[\d\.]+$/.test(date) && moment(date, 'YY.MM.DD.').toISOString(true);
        })();
        const createdAt = (() => {
          const el = tr.querySelector('td:nth-child(7)');
          const date = _.trim(el ? el.innerText : '');
          return /^[\d\.]+$/.test(date) && moment(date, 'YY.MM.DD.').toISOString(true);
        })();
        return { blogNo, blogId, blogUrl, blogName, nickName, group, type, lastPostAt, createdAt };
      });
      if(callback) callback(usernames);
      results.push(...usernames);
      return (has_next || has_next_valid) ? search_buddy_page(callback, page+1, results) : results.filter((o,i)=>results.indexOf(o)==i);
    }
    window.search_buddy = search_buddy_page;
  })
  GM_addScript(() => {
    async function search_buddy_me_page(callback, page = 1, results = []) {
      const blogId = new URL(location.href).searchParams.get('blogId') || location.pathname.split('/')[1];
      const res = await fetch(`https://admin.blog.naver.com/BuddyMeManage.naver?relation=all&blogId=${blogId}&currentPage=${page}`).then(r=>r.text());
      const doc = document.createElement('div'); doc.innerHTML = res;
      const pagination = Array.from(doc.querySelector('div.paginate_re').children), pagenation_last = pagination[pagination.length - 1];
      const has_next = pagenation_last.tagName == 'A' && !!pagenation_last.className, has_next_valid = pagenation_last.tagName != 'STRONG';
      const usernames_rows = Array.from(doc.querySelectorAll('tr a[href*="blog.naver.com"], tr a[href*=".blog.me"]'));
      const usernames = usernames_rows.map(e=>{
        const uri = new URL(e.href);
        const tr = e.closest('tr');
        const type = (() => {
          const el = tr.querySelector('td.groupwrap, td:nth-child(3)');
          const followed_type1 = el.querySelector('a > img[alt="서로이웃신청"]') ? '' : '서로이웃'
          const followed_type2 = el.querySelector('a > img[alt="이웃추가"]') ? '' : '이웃'
          return followed_type1 || followed_type2 || '비이웃';
        })();
        const blogNo = (() => tr.querySelector('td:first-child input').value)();
        const blogId = (() => {
          if(uri.hostname.includes('.blog.me')) return uri.hostname.replace('.blog.me', '');
          if(uri.hostname.includes('blog.naver.com')) return uri.pathname.replace('/', '');
          return uri.hostname;
        })();
        const blogUrl = (() => e.href)();
        const blogName = (() => {
          const el = tr.querySelector('td.buddy a, td:nth-child(2) a');
          return _.trim(el ? el.innerText : blogId);
        })();
        const nickName = (() => {
          const el = tr.querySelector('td.buddy span.nickname, td:nth-child(2) span.nickname');
          return _.trim(el ? el.innerText : blogId);
        })();
        const createdAt = (() => {
          const el = tr.querySelector('td.date, td:nth-child(4)');
          const date = _.trim(el ? el.innerText : '');
          return /^[\d\.]+$/.test(date) && moment(date, 'YY.MM.DD.').toISOString(true);
        })();
        return { blogNo, blogId, blogUrl, blogName, nickName, type, createdAt };
      });
      if(callback) callback(usernames);
      results.push(...usernames);
      return (has_next || has_next_valid) ? search_buddy_me_page(callback, page+1, results) : results.filter((o,i)=>results.indexOf(o)==i);
    }
    window.search_buddy_me = search_buddy_me_page;
  })
  GM_addScript(() => {
    const html = `
    <!DOCTYPE html>
    <html ng-app="app">
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/5.0.0-beta1/css/bootstrap.min.css" integrity="sha512-thoh2veB35ojlAhyYZC0eaztTAUhxLvSZlWrNtlV01njqs/UdY3421Jg7lX0Gq9SRdGVQeL8xeBp9x1IPyL1wQ==" crossorigin="anonymous" />
        <style>.loading { pointer-events: none; opacity: 0.5; }</style>
        <title>네이버 블로그 이웃,그룹 관리 어드밴스드</title>
      </head>
      <body>
        <div ng-controller="main" class="container-fluid">
          <div class="alert alert-info text-center">나를 이웃한 사람 : {{ followers.length }}명 / 내가 이웃한 사람 : {{ following.length }}명</div>
          <div class="p-1">
            <div class="row p-1">
              <div class="col-4">
                <select ng-model="filter.type" class="form-select" aria-label="이웃필터">
                  <option value="전체">전체</option>
                  <option value="서로이웃">서로이웃</option>
                  <option value="나도이웃">나도이웃</option>
                  <option value="나만이웃">나만이웃</option>
                </select>
              </div>
              <div class="col">
                <div class="input-group mb-3">
                  <span class="input-group-text">이웃추가일</span>
                  <input type="date" ng-model="filter.start" class="form-control" aria-label="등록일 시작">
                  <input type="date" ng-model="filter.limit" class="form-control" aria-label="등록일 종료">
                </div>
              </div>
            </div>
            <div class="row p-1">
              <div class="col-4">
                <select ng-model="filter.keywordType" class="form-select" aria-label="키워드 필터 상세">
                  <option value="blogId">아이디</option>
                  <option value="blogName">블로그명</option>
                  <option value="nickName">닉네임</option>
                  <option value="group">그룹명</option>
                </select>
              </div>
              <div class="col">
                <div class="input-group mb-3">
                  <span class="input-group-text">필터링단어</span>
                  <input type="text" ng-model="filter.keyword" placeholder="필터링 할 단어를 검색해주세요." class="form-control" aria-label="키워드 필터">
                </div>
              </div>
            </div>
          </div>
          <div class="p-1">
            <table class="table table-striped" ng-class="{loading: loading}">
              <thead>
                <tr>
                  <th scope="col">그룹명</th>
                  <th scope="col">이웃상태</th>
                  <th scope="col">닉네임 (아이디), 블로그명</th>
                  <th scope="col">최근작성</th>
                  <th scope="col">이웃추가</th>
                  <th scope="col">관리메뉴</th>
                </tr>
              </thead>
              <tbody>
                <tr ng-repeat="user in following | filter_follow:filter:followers">
                  <td>{{ user.group }}</td>
                  <td>{{ user.type }}</td>
                  <td>
                    <div>{{ user.nickName }} ({{user.blogId}})</div>
                    <div><small><a target="_blank" ng-href="{{user.blogUrl}}">{{ user.blogName }}</a></small></div>
                  </td>
                  <td>{{ user.lastPostAt.substr(0, 10) }}</td>
                  <td>{{ user.createdAt.substr(0, 10) }}</td>
                  <td>
                    <button ng-click="remove(user)" class="btn btn-sm btn-warning">삭제</button>
                    <button ng-click="cutout(user)" class="btn btn-sm btn-danger">차단</button>
                  </td>
                </tr>
              </tbody>
            </table>
          </div>
        </div>


        <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment-with-locales.min.js" integrity="sha512-LGXaggshOkD/at6PFNcp2V2unf9LzFq6LE+sChH7ceMTDP0g2kn6Vxwgg7wkPP7AAtX+lmPqPdxB47A0Nz0cMQ==" crossorigin="anonymous"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.21/lodash.min.js" integrity="sha512-WFN04846sdKMIP5LKNphMaWzU7YpMyCU245etK3g/2ARYbPK9Ub18eG+ljU96qKRCWh+quCY7yefSmlkQw1ANQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.0.2/js/bootstrap.bundle.min.js" integrity="sha512-72WD92hLs7T5FAXn3vkNZflWG6pglUDDpm87TeQmfSg8KnrymL2G30R7as4FmTwhgu9H7eSzDCX3mjitSecKnw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/angular.js/1.8.2/angular.min.js" integrity="sha512-7oYXeK0OxTFxndh0erL8FsjGvrl2VMDor6fVqzlLGfwOQQqTbYsGPv4ZZ15QHfSk80doyaM0ZJdvkyDcVO7KFA==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
        <script>
          const app = angular.module('app', []);
          app.constant('_', window._);
          app.constant('moment', window.moment);
          app.run(($rootScope) => {
            $rootScope._ = window._;
            $rootScope.moment = window.moment;
          })
          app.filter('filter_follow', () => {
            return function(following, filter, followers) {
              return following
              .filter((o) => {
                // filter.start, filter.limit
                const createdAt = new Date(o.createdAt);
                return filter.start <= createdAt && createdAt <= filter.limit;
              })
              .filter((o) => {
                // filter.keywordType && filter.keyword
                const state = o[filter.keywordType];
                return state && state.includes(filter.keyword);
              })
              .filter((o) => {
                // filter.type
                if(filter.type == '전체') return true;
                if(filter.type == '서로이웃') return o.type == '서로이웃';
                if(filter.type == '나도이웃') return o.type == '이웃' && !!followers.find(f=>f.blogId == o.blogId);
                if(filter.type == '나만이웃') return o.type == '이웃' &&  !followers.find(f=>f.blogId == o.blogId);
              });
            }
          });
          app.controller('main', ($scope) => {
            $scope.loading = true;
            $scope.filter = $scope.filter || {}
            $scope.filter.type = '전체';
            $scope.filter.keywordType = 'blogId';
            $scope.filter.keyword = '';
            $scope.filter.start = new Date(1970, 0, 1);
            $scope.filter.limit = new Date();

            $scope.followers = [];
            $scope.following = [];
            document.addEventListener('append.following', (e)=>($scope.following.push(...e.detail.data), $scope.$apply()), false);
            document.addEventListener('append.followers', (e)=>($scope.followers.push(...e.detail.data), $scope.$apply()), false);
            document.addEventListener('append.finish', (e)=>($scope.loading = false, $scope.$apply()), false);

            $scope.remove = async (user) => {
              if(confirm('정말로 이웃을 삭제하시겠습니까. 이 동작은 취소 할 수 없습니다.')) {
                $scope.followers = $scope.followers.filter((o)=>o.blogId != user.blogId);
                $scope.following = $scope.following.filter((o)=>o.blogId != user.blogId);
                await window.opener.delete_buddy([user]);
              }
            };
            $scope.cutout = async (user) => {
              if(confirm('정말로 이웃을 삭제하시겠습니까. 이 동작은 취소 할 수 없습니다.')) {
                $scope.followers = $scope.followers.filter((o)=>o.blogId != user.blogId);
                $scope.following = $scope.following.filter((o)=>o.blogId != user.blogId);
                await window.opener.delete_buddy([user]);
                
                const blogId = new URL(window.opener.location.href).searchParams.get('blogId') || window.opener.location.pathname.split('/')[1];
                const uri = new URL('https://admin.blog.naver.com/BuddyMultiBlockForm.naver?relation=all&currentPage=1');
                uri.searchParams.set('blogId', blogId);
                uri.searchParams.append('targetBlogId', user.blogId);
                
                const html = "<scri"+"pt>window.open('" + uri.toString() + "', 'popupWindow', 'width=330, height=220');</scr"+"ipt>";
                const blob = new Blob([html], { type: 'text/html' });
                const blob_url = URL.createObjectURL(blob);
                window.open(blob_url, 'popupWindow', 'width=330, height=220');
              }
            };
          });
          angular.bootstrap(document, ['app']);
        </script>
      </body>
    </html>
    `;
    const $window = window.open('about:blank', '_buddy_window', 'width=960,height=720');
    setTimeout(async ()=>{
      $window.document.write(html);
      setTimeout(async () => {
        await search_buddy((data) => {
          if($window.closed) throw new Error('closed');
          $window.document.dispatchEvent(new CustomEvent('append.following', { detail: { data } }));
        }).catch(e=>null);
        await search_buddy_me((data) => {
          if($window.closed) throw new Error('closed');
          $window.document.dispatchEvent(new CustomEvent('append.followers', { detail: { data } }))
        }).catch(e=>null);
        $window.document.dispatchEvent(new CustomEvent('append.finish'))
      }, 300);
    }, 300);
  })
});